首頁  >  文章  >  web前端  >  JavaScript中this綁定詳解

JavaScript中this綁定詳解

高洛峰
高洛峰原創
2016-10-13 09:35:021013瀏覽

this 可以說是javascript 中最耐人尋味的一個特性,就像高中英語裡各種時態,比如被動時態,過去時,現在時,過去進行時一樣,無論弄錯過多少次,下一次依然可能弄錯。本文啟發於《你不知道的JavaScript上卷》,對 javasript 中的 this 進行一個總結。

學習 this 的第一步就是明白 this 既不是指向函數本身也不指向函數的作用域。 this 實際上是在函數被呼叫時發生的綁定,它指向什麼地方完全取決於函數在哪裡被呼叫。

預設綁定

在 javascript 中 ,最常用的函數調用類型就是獨立函數調用,因此可以把這條規則看作是無法應用其他規則時的預設規則。如果在調用函數的時候,函數不帶任何修飾,也就是「光禿禿」的調用,那就會應用預設綁定規則, 預設綁定的指向的是全域作用域。

function sayLocation() { 
    console.log(this.atWhere) 
} 
 
var atWhere = "I am in global" 
 
sayLocation() // 默认绑定,this绑定在全局对象,输出 “I am in global”

再看一個例子

var name = "global" 
function person() { 
    console.log(this.name) //  (1) "global" 
      person.name = 'inside' 
    function sayName() { 
        console.log(this.name) // (2) "global"  不是 "inside" 
    } 
    sayName() // 在person函数内部执行sayName函数,this指向的同样是全局的对象 
} 
person()

在這個例子中,person 函數在全局作用域中被調用,因此第(1)句中的this 就綁定在了全局對像上(在瀏覽器中是是window ,在node中就是global),因此第(1)句自然輸出的是一個全域物件的name 屬性,當然就是"global"了。 sayName函數在person函數內調用,即使這樣第(2)句中的this指代的仍然是全域對象,即使 person 函數設定了 name 屬性。

這就是預設綁定規則,它是 javascript 中最常見的一種函數呼叫模式,this 的綁定規則也是四種綁定規則中最簡單的一種,就是綁定在全域作用域上。

預設綁定裡的嚴格模式

在 javascript 中,如果使用了嚴格模式,則 this 不能綁定到全域物件。還是以第一個例子,只不過這次加上了嚴格模式聲明

'use strict' 
function sayLocation() { 
    console.log(this.atWhere) 
} 
var atWhere = "I am in global" 
sayLocation() 
// Uncaught TypeError: Cannot read property 'atWhere' of undefined

可以看出,在嚴格模式下,把this 綁定到全局對像上時,實際上綁定的是undefined ,因此上面這段代碼會報錯誤。

隱式綁定

當函數在呼叫時,如果函數有所謂的「落腳點」,即有上下文物件時,隱式綁定規則會把函數中的 this 綁定到這個上下文物件。如果覺得上面這段話不夠直白的話,還是來看程式碼。

function say() { 
    console.log(this.name) 
} 
var obj1 = { 
    name: "zxt", 
    say: say 
} 
 
var obj2 = { 
    name: "zxt1", 
    say: say 
} 
obj1.say() // zxt 
obj2.say() // zxt

很簡單是不是。在上面這段程式碼中,obj1 , obj2 就是所謂的 say 函數的落腳點,專業一點的說法就是上下文對象,當給函數指定了這個上下文對象時,函數內部的this 自然指向了這個上下文對象。這也是很常見的一種函數呼叫模式。

隱式綁定時丟失上下文

function say() { 
    console.log(this.name) 
} 
var name = "global" 
var obj = { 
    name: "inside", 
    say: say 
} 
var alias = obj.say // 设置一个简写   (1)  
alias() // 函数调用 输出"global"  (2)

可以看到這裡輸出的是」global「 ,為什麼就和上例中不一樣,我們明明只是給obj.say 換了個名字而已?

首先我們來看上面第(1)句程式碼,由於在javascript 中,函數是對象,物件之間是參考傳遞,而不是值傳遞。因此,第(1)句程式碼只是alias = obj.say = say ,也就是alias = say ,obj.say 只是起了一個橋樑的作用,alias 最終引用的是say 函數的位址,而與obj 這個物件無關了。這就是所謂的」丟失上下文「。最後執行 alias 函數,只不過簡單的執行了say函數,輸出"global"。

顯式綁定

顯式綁定,顧名思義,顯示地將this綁定到一個上下文,javascript中,提供了三種顯式綁定的方法,apply,call,bind。 apply和call的用法基本上相似,它們之間的區別是:

apply(obj,[arg1,arg2,arg3,...] 被呼叫函數的參數以數組的形式給出

call(obj,arg1 ,arg2,arg3,...) 被呼叫函數的參數依序給出

而bind函數執行後,返回的是一個新函數。就是給函數綁定一個執行上下文,且是明確綁定的。用法上的差異。內的this指定了執行上下文,而傳回這個新函數可以接受參數。 。操作符調用而已.

function Person(name,age) { 
    this.name = name 
    this.age = age 
    console.log("我也只不过是个普通函数") 
} 
Person("zxt",22) // "我也只不过是个普通函数" 
console.log(name) // "zxt" 
console.log(age) // 22 
 
var zxt = new Person("zxt",22) // "我也只不过是个普通函数" 
console.log(zxt.name) // "zxt" 
console.log(zxt.age) // 22

上面这个例子中,首先定义了一个 Person 函数,既可以普通调用,也可以以构造函数的形式的调用。当普通调用时,则按照正常的函数执行,输出一个字符串。 如果是通过一个new操作符,则构造了一个新的对象。那么,接下来我们再看看两种调用方式, this 分别绑定在了何处首先普通调用时,前面已经介绍过,此时应用默认绑定规则,this绑定在了全局对象上,此时全局对象上会分别增加name 和 age 两个属性。当通过new操作符调用时,函数会返回一个对象,从输出结果上来看 this 对象绑定在了这个返回的对象上。

因此,所谓的new绑定是指通过new操作符来调用函数时,会产生一个新对象,并且会把构造函数内的this绑定到这个对象上。

事实上,在javascript中,使用new来调用函数,会自动执行下面的操作。

1.创建一个全新的对象

2.这个新对象会被执行原型连接

3.这个新对象会绑定到函数调用的this

4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

四种绑定的优先级

上面讲述了javascript中四种this绑定规则,这四种绑定规则基本上涵盖了所有函数调用情况。但是如果同时应用了这四种规则中的两种甚至更多,又该是怎么样的一个情况,或者说这四种绑定的优先级顺序又是怎么样的。

首先,很容易理解,默认绑定的优先级是最低的。这是因为只有在无法应用其他this绑定规则的情况下,才会调用默认绑定。那隐式绑定和显式绑定呢?还是上代码吧,代码可从来不会说谎。

function speak() { 
    console.log(this.name) 
} 
 
var obj1 = { 
    name: 'obj1', 
    speak: speak 
} 
var obj2 = { 
    name: 'obj2' 
} 
 
obj1.speak() // obj1 (1) 
obj1.speak.call(obj2) // obj2 (2)

所以在上面代码中,执行了obj1.speak(),speak函数内部的this指向了obj1,因此(1)处代码输出的当然就是obj1,但是当显式绑定了speak函数内的this到obj2上,输出结果就变成了obj2,所有从这个结果可以看出显式绑定的优先级是要高于隐式绑定的。事实上我们可以这么理解obj1.speak.call(obj2)这行代码,obj1.speak只是间接获得了speak函数的引用,这就有点像前面所说的隐式绑定丢失了上下文。好,既然显式绑定的优先级要高于隐式绑定,那么接下来再来比较一下new 绑定和显式绑定。

function foo(something) { 
    this.a = something 
} 
 
var obj1 = {} 
var bar = foo.bind(obj1)  // 返回一个新函数bar,这个新函数内的this指向了obj1  (1) 
bar(2) // this绑定在了Obj1上,所以obj1.a === 2 
console.log(obj1.a) 
 
var baz = new bar(3)  // 调用new 操作符后,bar函数的this指向了返回的新实例baz  (2) 
 
console.log(obj1.a) 
console.log(baz.a)

我们可以看到,在(1)处,bar函数内部的this原本指向的是obj1,但是在(2)处,由于经过了new操作符调用,bar函数内部的this却重新指向了返回的实例,这就可以说明new 绑定的优先级是要高于显式绑定的。

至此,四种绑定规则的优先级排序就已经得出了,分别是

new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

箭头函数中的this绑定

箭头函数是ES6里一个重要的特性。

箭头函数的this是根据外层的(函数或者全局)作用域来决定的。函数体内的this对象指的是定义时所在的对象,而不是之前介绍的调用时绑定的对象。举一个例子

var a = 1 
var foo = () => { 
    console.log(this.a) // 定义在全局对象中,因此this绑定在全局作用域 
} 
 
var obj = { 
    a: 2 
} 
foo() // 1 ,在全局对象中调用 
foo.call(obj) // 1,显示绑定,由obj对象来调用,但根本不影响结果

从上面这个例子看出,箭头函数的 this 强制性的绑定在了箭头函数定义时所在的作用域,而且无法通过显示绑定,如apply,call方法来修改。在来看下面这个例子

// 定义一个构造函数 
function Person(name,age) { 
    this.name = name 
    this.age = age  
    this.speak = function (){ 
        console.log(this.name) 
        // 普通函数(非箭头函数),this绑定在调用时的作用域 
    } 
    this.bornYear = () => { 
        // 本文写于2016年,因此new Date().getFullYear()得到的是2016 
        // 箭头函数,this绑定在实例内部 
        console.log(new Date().getFullYear() - this.age) 
        } 
    } 
} 
 
var zxt = new Person("zxt",22) 
 
zxt.speak() // "zxt" 
zxt.bornYear() // 1994 
 
// 到这里应该大家应该都没什么问题 
 
var xiaoMing = { 
    name: "xiaoming", 
    age: 18  // 小明永远18岁 
} 
 
zxt.speak.call(xiaoMing) 
// "xiaoming" this绑定的是xiaoMing这个对象 
zxt.bornYear.call(xiaoMing) 
// 1994 而不是 1998,这是因为this永远绑定的是zxt这个实例

因此 ES6 的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this ,具体来说就是,箭头函数会继承 外层函数调用的this绑定 ,而无论外层函数的this绑定到哪里。

小结

以上就是javascript中所有this绑定的情况,在es6之前,前面所说的四种绑定规则可以涵盖任何的函数调用情况,es6标准实施以后,对于函数的扩展新增了箭头函数,与之前不同的是,箭头函数的作用域位于箭头函数定义时所在的作用域。

而对于之前的四种绑定规则来说,掌握每种规则的调用条件就能很好的理解this到底是绑定在了哪个作用域。


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn