写在之前
this作为一个被自动定义在所有函数作用域中的关键字,它是JavaScript中最复杂的机制之一,因此对它进行再多次的研究也不为过。
this
this到底起了哪些作用
假设一个场景,我们现在需要引用不同对象的中的属性。(只讨论普通函数)。
var name = 'global';
const obj1 = {
name: 'obj1',
}
const obj2 = {
name: 'obj2',
}
- 一种很自然的想法是在不同的上下文对象中调用对应的函数,这样函数的this将会自动绑定到这些对象上。
function foo(){
return 'hello ' + this.name;
}
function bar(){
console.log(foo.call(this))
}
bar.call(obj1); //hello obj1
bar() //hello global
- 第二种不使用this的方法,则需要我们手动传入一个context参数。
function foo(context)
{
return 'hello ' + context.name;
}
function bar(context){
console.log(foo(context))
}
bar(obj1)
bar(window)
- 因此我们可以看出,this可以以一种优雅的方式隐式绑定上下文对象,并且由于this可以在不同情况下应用不同规则指向不同的上下文对象,这样避免了当需求越来越复杂,使用的context越来越多的情况。
绑定规则
绑定规则的作用
- 绑定规则将决定this绑定在哪个上下文对象。
4条绑定规则
-
默认绑定
-
生效条件
- 当函数独立调用时将应用默认绑定规则,并且当不满足其他3条绑定规则时,也将应用默认绑定。(在严格模式下,将指向undefined)。
-
作用
- 将函数的this关键字绑定到全局对象(是全局对象而不是全局作用域)。
-
var name = 'global';
const obj1 = {
name: 'obj1',
}
const obj2 = {
name: 'obj2',
}
function foo(){
console.log(this.name)
}
foo(); //global
-
隐式绑定
-
生效条件
- 当函数调用时,函数被某一个上下文对象引用。
-
作用
- 将函数的this关键字绑定到引用函数的上下文对象。
-
var name = 'global';
const obj1 = {
name: 'obj1',
}
function foo(){
console.log(this.name)
}
obj1.foo = foo;
obj1.foo(); //obj1
-
显式绑定
-
生效条件
- 在调用函数时使用apply、call。
-
作用
- 将函数的this关键字绑定到提供的上下文对象。
var name = 'global'; const obj1 = { name: 'obj1', } function foo(){ console.log(this.name) } foo.call(obj1); //obj1
-
硬绑定
-
生效条件
- 在调用函数时使用bind。
-
作用
- 硬绑定是显式绑定的一个变种,用于解决this丢失的问题。
-
var name = 'global'; const obj1 = { name: 'obj1', } function foo(){ console.log(this.name) } function Bind(obj){ return foo.call(obj,[].slice.call(arguments,1)) } Bind(obj1); //obj1 Bind(); //global
-
[].slice.call,[]的作用是使JS引擎可以通过原型对象访问到slice方法,接着使用call将slice函数中的this关键字绑定到arguments上,这样就实现了实际上对arguments类数组调用slice方法的目的。
-
这样做的好处是可以不用使用Array.from()将arguments类数组对象转成数组对象再访问slice方法。
-
Bind(),直接独立调用包装函数Bind相当于为显式绑定call传入了undefined,因此将会应用默认绑定规则。
-
-
new绑定
-
生效条件
- new绑定在使用new关键字调用函数时生效。
-
作用
- new绑定将把被当成构造函数调用的函数的this指向创建的新对象。
-
function Foo(){
this.name = 'a new object';
}
const obj = new Foo()
console.log(obj.name) //a new object
- new操作符的执行原理
- 创建一个新对象
- 将新对象的[[Prototype]]特性指向构造函数的prototype属性指向的原型对象。
- 将构造函数的this绑定到新对象上,这样构造函数中的一切this相关语句都将在新对象上执行。
- 执行构造函数。
- 如果构造函数没有return一个对象,那么返回这个新对象。
function Foo(){
this.name = 'new object';
const obj = {};
obj.name = 'object from constructor';
return obj;
}
const obj = new Foo();
console.log(obj.name); //object from constructor
可以看到因为构造函数返回了一个对象,因此新对象被忽略了,最终会被垃圾回收。
常见问题
隐式丢失
-
隐式丢失是指应用隐式绑定规则的函数的this关键字可能会丢失绑定对象。这时会应用默认规则。
-
常发生隐式丢失的情况
- 使用函数别名调用函数。
const obj1 = { name: 'obj1', foo: function(){ console.log(this.name) } } var name = 'global'; const bar = obj1.foo; bar(); //global
这是因为const bar = obj1.foo;这个赋值语句将bar标识符指向了堆内存中的Function实例,直接调用bar()相当于独立函数调用。
- 被当成是回调函数的函数。
const obj1 = { name: 'obj1', foo: function(){ console.log(this.name) } } var name = 'global' setTimeout(obj1.foo,1000) //global
这实际上是因为在传入回调函数的时候有一个隐式赋值,因此实际上逻辑和上面的函数别名一致。
-
解决方案
-
使用函数别名导致隐式丢失的情况
- 可以应用显式绑定解决。
const obj1 = { name: 'obj1', foo: function(){ console.log(this.name) } } var name = 'global'; const bar = obj1.foo; bar.call(obj1); //obj1
-
回调函数导致隐式丢失的情况
const obj1 = { name: 'obj1', foo: function(){ console.log(this.name) } } var name = 'global' function callFn(fn) { fn(); } callFn(obj1.foo.call(obj1)); //TypeError,fn is not a function
这是因为JS引擎在执行到callFn(obj1.foo.call(obj1));时,发现需要对其进行隐式赋值,也就是fn = obj1.foo.call(obj1),这个时候,fn实际上是字符串'obj1',因为在赋值之前obj1.foo.call(obj1)就被执行。因此fn接收的不是一个可调用的类型,因此出现TypeError错误。
所以call和apply的显式绑定无法解决回调导致的this丢失。但是我们想到bind可以返回一个函数,因此可以使用硬绑定bind。
- 使用硬绑定
const obj1 = { name: 'obj1', foo: function(){ console.log(this.name) } } var name = 'global' setTimeout(obj1.foo.bind(obj1),1000) //obj1
-
被忽略的this
- 当把null、undefined作为绑定对象传入call、apply、bind时,将应用默认绑定规则。
绑定规则优先级
new绑定优先级最高。
显示绑定或硬绑定。
隐式绑定。
默认绑定优先级最低。
箭头函数
回顾定义
- 箭头函数是ES6引入的由胖剪头定义的函数。
剪头函数的this
- 箭头函数的this不适用绑定规则,它的this关键字将从外层词法作用域继承,也就是当需要在箭头函数中使用this时,将应用词法作用域查找规则。实际上箭头函数和ES6之前的self = this作用是一致的。
- 在这里不需要担心外层词法作用域的this指向不明确的问题,由于词法作用域的限制,当我们可以访问位于父级词法作用域中的箭头函数标识符时,这个父级词法作用域或者说父级函数的活动对象必然存在于作用域链,而这一切的前提是父级函数被调用执行,因此其this指向必然是明确的。
const obj1 = {
name: 'obj1',
foo: function(){
return (() => {
console.log(this.name)
})()
}
}
const obj2 = {
name: 'obj2',
foo: () => {
console.log(this.name)
}
}
const obj3 = {
name: 'obj3',
foo: function(){
return () => {
console.log(this.name)
}
}
}
var name = 'global';
obj1.foo(); //obj1
obj2.foo(); //global
const bar = obj3.foo();
bar(); //obj3
-
在obj1中foo属性指向一个匿名函数,这个匿名函数返回一个箭头函数IIFE。 箭头函数的this将从它的上层词法作用域中继承,这里箭头函数的上层词法作用域是匿名函数,匿名函数的this关键字隐式绑定到obj1上下文对象,因此箭头函数的this将始终指向obj1。
-
在obj2中foo属性指向一个箭头函数。 执行obj2.foo()时,虽然是在obj2的引用下调用的,但是由于箭头函数的this绑定不遵循4条绑定规则,因此箭头函数的this会指向上一层词法作用域,也就是全局作用域的this,因此指向全局对象。
-
在obj3中foo属性指向一个匿名函数,这个匿名函数返回一个箭头函数。 虽然这里使用了函数别名调用这个箭头函数,但是箭头函数的this会遵循词法作用域查找规则,去上层词法作用域查找this,因此箭头函数的this会继承obj3种的foo属性指向的匿名函数中的this,而匿名函数的this指向引用它的上下文对象obj3。
函数作用域和函数
- 实际上函数每调用一次就会在当前作用域链的末尾形成一个独立的函数作用域。也就是说函数每调用一次都会在当前词法作用域内部嵌套一个子词法作用域。
const obj3 = {
name: 'obj3',
foo: function(){
return () => {
console.log(this)
}
}
}
const bar = obj3.foo(); //产生了一个函数作用域,并且内部this指向obj3
bar(); //obj3
const foo1 = obj3.foo;
const bar1 = foo1(); //产生了一个函数作用域,并且内部this指向window。
bar1(); //window
从上面我们可以看到,由于每次函数被调用都产生了一个独立的函数作用域,其中的this指向根据情况有所不同,因此箭头函数在进行继承this的时候也会有所不同。