1 this
的五种绑定方式
1.1 默认绑定
默认绑定是指当函数调用时,没有为其指定对象上下文,此时会将该函数的this
绑定到全局对象(window
对象)。自ES5有了严格模式之后,默认绑定方式又分为非严格模式的默认绑定和严格模式的默认绑定。
1.1.1 非严格模式
在非严格模式下,函数的默认绑定只能绑定到全局对象window
,见下面例子:
var myName = 'syzdev' // 相当于 window.myName = 'syzdev'
function sayName() {
// 这里的 this === window
console.log(this === window); // true
console.log(this.myName); // syzdev
}
sayName()
在这个例子中,首先声明了一个全局变量myName
:
var
声明的变量为全局变量,并且会将该变量添加为全局对象window
的属性,需要注意的是,例子中的var
不可修改为ES6中的let
或const
,不然结果大有不同,有兴趣的读者可以查阅资料“var
、let
、const
的区别”。
再定义了一个sayName()
函数,在函数中输出this.myName
,最后直接调用sayName()
函数,再调用时,由于没有指定其对象上下文,所以触发了默认绑定,即将函数中的this
绑定到全局对象window
上,因此在函数内this === window
,最后输出的this.myName
为 syzdev
。
1.1.2 严格模式
关于严格模式,这里不做介绍,若不了解的读者可以阅读MDN-严格模式。
在严格模式下,有一项规定为“禁止this
关键字指向全局对象window
”,此时this
会绑定到undefined
,见下面例子:
// 开启严格模式
'use strict'
var myName = 'syzdev'
function sayName() {
// 这里的 this !== window
console.log(this === window); // false
console.log(this); // undefined
console.log(this.myName); // 报错:Uncaught TypeError: Cannot read properties of undefined (reading 'myName')
}
sayName()
在这个例子中,在代码顶部使用'use strict'
声明了严格模式,由于在严格模式下禁止this
绑定到全局对象window
,所以函数中的this !== window
,输出this
为undefined
,因此输出this.myName
时会直接抛出错误“Uncaught TypeError: Cannot read properties of undefined (reading ‘name’)”。
由于严格模式可以声明为全局或个别函数内,上面的例子中就是声明为全局严格模式,若声明为函数内严格模式,伪代码如下:
function sayName() {
'use strict'
...
}
需要注意的是,严格模式并不会影响到调用函数内的默认绑定,见下面例子:
var myName = 'syzdev'
function sayName() {
// 这里的 this === window
console.log(this === window); // true
console.log(this.myName); // syzdev
}
function strictSayName() {
// 函数内的严格模式
'use strict'
sayName()
}
strictSayName()
在这个例子中,定义函数strictSayName()
并在函数内部声明严格模式,再调用sayName()
函数,此时sayName()
函数内部依然能够将this
绑定到全局对象window
上。
1.2 隐式绑定
隐式绑定是日常开发中最为常见的绑定方式,即调用函数时为函数指明其对象上下文,此时函数中的this
就为对象本身,见下面例子:
var personObj = {
myName: 'syzdev',
sayName: function() {
console.log(this === personObj) // true
console.log(this.myName); // syzdev
}
}
personObj.sayName()
在这个例子中,定义一个personObj
对象,包含myName
属性和sayName()
方法,通过对象personObj.sayName()
调用函数,此时就会触发this
隐式绑定,函数中的this
就是personObj
本身。
但隐式绑定还可能会出现绑定丢失的情况,此时隐式绑定就会变成了默认绑定,见下面例子:
var personObj = {
myName: '123',
sayName: function() {
console.log(this === window); // true
console.log(this.myName); // undefined
}
}
var anotherSayName = personObj.sayName
anotherSayName()
在这个例子中,把personObj.sayName()
函数赋值给了变量anotherSayName
,再直接使用anotherSayName()
调用,此时虽然调用的还是sayName()
函数,但是却丢失了对象上下文,此时隐式绑定变成了默认绑定,anotherSayName()
函数内的this
绑定到全局对象window
上,由于window
对象上没有myName
属性,所以输出undefined
。
1.3 显式绑定
显示绑定顾名思义,通过调用call
/apply
/bind
方法,强制将函数中的this
绑定到指定的对象,三者区别如下:
call()
接受的是多个对象作为参数。apply()
接受的是多个对象组成的数组作为参数。bind()
返回的是一个函数,除此之外使用方法与call()
一样。
使用call
/apply
/bind
实现显示绑定的方法如下:
- 不含参数的使用方法:
function foo() {
console.log(this.myName);
}
var myName = 'window myName'
var personObj = {
myName: 'personObj myName'
}
foo() // window myName
foo.call(personObj) // personObj myName
foo.apply(personObj) // personObj myName
foo.bind(personObj)() // personObj myName
在这个例子中,直接调用sayName()
函数会触发默认绑定,函数内的this
绑定到全局对象window
,所以输出window myName
。由于该例子中函数不需要传参,所以在使用call
/apply
/bind
方法时,不需要为其传递除this
对象外的其他参数,其效果都是一致的,都是将sayNname()
函数内的this
绑定到personObj
对象,最后输出的myName
为personObj
对象中的personObj myName
。
- 含参数的使用方法:
function foo(name, age) {
console.log(name, age)
}
foo('syzdev', 18) // 直接调用函数传递参数
foo.call(this, 'syzdev', 18) // call执行函数,第一个参数指定this对象,后续参数依次为函数的传参
foo.apply(this, ['syzdev', 18]) // apply执行函数,第一个参数指定this对象,第二个参数为一个数组,数组中为函数的传参
foo.bind(this, 'syzdev', 18)() // bind执行函数,返回的是一个函数,除此之外用法与call相同
// 输出结果
// syzdev 18
// syzdev 18
// syzdev 18
// syzdev 18
代码详解见注释。
1.4 new
绑定
在ES6以前,生成一个实例对象的方法是使用构造函数,构造函数只是一个普通的函数,当使用new
操作符调用构造函数时,JavaScript解释器便会在底层创建一个新对象,构造函数内的this
绑定的就是这个新对象,见下面例子:
function Person(name, age) {
this.name = name
this.age = age
}
var person = new Person('syzdev', 18)
在这个例子中,使用new
操作符执行一个构造函数,创建一个person
对象,在构造函数内会创建一个新的对象,将函数内的this
绑定到这个新对象上,如果函数内部没有返回其他对象,则构造函数会默认返回这个新对象,流程如下图所示:
1.5 箭头函数绑定
箭头函数是ES6中的一个重要特性,在箭头函数中没有自己的this
,其this
是根据外层的作用域来决定的,箭头函数内的this
指的是定义时所在的对象,是在函数定义时已经决定了,而不是像普通函数一样在调用时绑定this
对象,见下面例子:
var foo = () => {
console.log(this.myName);
}
var myName = 'window myName'
var personObj = {
myName: 'personObj myName'
}
foo() // window myName
foo.call(personObj) // window myName
foo.apply(personObj) // window myName
foo.bind(personObj)() // window myName
在这个例子中,函数foo
定义为箭头函数,由于箭头函数没有自己的this
,其this
值在定义时已经决定,所以无法被call
/apply
/bind
方法所改变,在该例子中其this
值为全局对象window
,所以四种方法调用foo()
函数的结果都为window myName
。
2 this
绑定的优先级
所谓this
绑定的优先级,指的是当函数执行时若同时指定了多个this
对象时的绑定顺序。
- 隐式绑定 > 默认绑定
在第2章的例子中已经证明了这个结论:
var personObj = {
myName: 'syzdev',
sayName: function() {
console.log(this === personObj) // true
console.log(this.myName); // syzdev
}
}
// 相当于 window.personObj.sayName()
personObj.sayName()
在这个例子中,personObj.sayName()
为隐式绑定,而personObj.sayName()
相当于window.personObj.sayName()
可见window
对象的默认绑定失效了,最终执行的this
依旧是personObj
,所以隐式绑定 > 默认绑定。
- 显示绑定 > 隐式绑定
修改第3章的例子如下:
function foo() {
console.log(this.myName);
}
var myName = 'window myName'
var personObj = {
myName: 'personObj myName',
foo: foo // 修改部分
}
personObj.foo.call(window) // window myName
在上面例子中的personObj.foo
为隐式绑定,再通过call
将foo()
函数中的this
显示绑定到全局对象window
上,最终输出的结果还是window myName
,可见显示绑定 > 隐式绑定。
new
绑定 > 显示绑定
function foo(name) {
this.name = name
}
// 创建对象obj
var obj = {}
// 将obj绑定为函数foo中的this
var bindFoo = foo.bind(obj)
bindFoo('syzdev')
console.log(obj.name) // syzdev
var bindObj = new bindFoo('bind syzdev')
console.log(bindObj.name) // bind syzdev
在这个例子中,使用bind
方法将obj
绑定到foo()
函数中并返回一个新的函数bindFoo
,在使用new
操作符创建一个新的对象bindObj
时,函数bindFoo()
的this
指向发生了改变,原本函数bindFoo()
中的this
指向的是obj
,在使用new
操作符后指向了一个新的对象并返回赋值给了bindObj
。
2.2 总结
在讨论this
绑定优先级时之所以不讨论箭头函数,是因为箭头函数的this
在其函数定义时就已经确定,不存在优先级一说。其他四种绑定方式的优先级如下,由高到低:
new
绑定- 显示绑定
- 隐式绑定
- 默认绑定