注册
web

你一定疑惑JavaScript中的this绑定的究竟是什么?😵‍💫

想要了解this的绑定过程,首先要理解调用方式

调用方式

调用方式被描述为函数被触发执行时语法形式

主要有以下几种基本模式:

  1. 直接调用(独立函数调用): f1()
  2. 方法调用: f1.f2()
  3. 构造函数调用: new f1()
  4. 显示绑定调用: f1.call(f2) 或者 f1.apply(f2)
  5. 间接调用: (0,f1)()

第五点可能很多人没有见过,其实这是应用了逗号操作符,(0,f1)()其实等同于f1(),但它有什么区别呢?我放在显式绑定的最后来阐述吧。

有的人会用调用位置来解释this的绑定,但我感觉那个不太好用,可能是我没理解到位吧,如果有人知道怎么用它来解释this的绑定,希望能告诉我。总之,我们先用调用方式来解释this的绑定吧。

四种绑定规则

接下来介绍四种绑定规则。

默认绑定

首先要介绍的是默认绑定,当使用了最常用的函数调用类型:直接调用(独立函数调用) 时,便应用默认绑定。可以把这条规则看作是无法应用其他规则时的默认规则。

在默认绑定时,this绑定的是全局作用域

var a = 0;
function f1(){
var a = 1;
console.log(this.a); //输出为0
}
f1(); //直接调用,应用默认绑定

多个函数内部层层调用也是一样的。

var a = 0;
function f1(){
var a = 1;
f2();
}
function f2(){
var a = 2;
console.log(this.a); //输出的是0
}
f1();

隐式绑定

当函数被当作对象的属性被调用时(例如通过obj.f1()的形式),this会自动绑定到该对象上,这个绑定是隐式发生的,不需要显式使用callapplybind

var a = 0;
function f1() {
var a = 1;
console.log(this.a); //this绑定的是f2这个对象字面量
}
var obj = {
a : 2,
f1 : f1

// 也可以直接在obj内部定义f1
// function f1() {
// var a = 1;
// }

};
obj.f1(); // 输出为2

对象层层引用只有最后一个对象会影响this的绑定

var a = 0;
function f1() {
var a = 1;
console.log(this.a); //最后输出为2
}
var obj1 = {
a : 2,
f1 : f1
};
var obj2 = {
a : 3,
obj1 : obj1
}
obj2.obj1.f1();

可以发现这里有两个对象一个是obj1,一个是obj2obj2中的属性为obj1。先通过ob2.obj1调用obj1,再通过ob2.obj1.f1()调用f1函数,可以发现对象属性引用链中的最后一个对象为this所绑定的对象

隐性丢失

但隐式绑定可能会导致this丢失所绑定的对象,也就是会应用默认绑定(this绑定到全局作用域) 造成隐性丢失主要有两个方面,一个是给函数取别名,一个是回调函数

  • 函数取别名
var a = 0;
function f1() {
var a = 1;
console.log(this.a); //最后输出为0
}
var obj = {
a : 2,
f1 : f1
}
var fOne = obj.f1; // 给f1取了一个fOne的别名
fOne();

虽然函数fOneobj.f1的一个引用,但实际上,它引用的是f1函数本身,因此它执行的就是f1()。所以会使用默认绑定。

  • 回调函数
var a = 0;
// f1为回调函数,将obj.f2作为参数传递给f1
function f1(f2) {
var a = 1;
f2();
}
function f2() {
var a = 2;
console.log(this.a); //结果为0
}
var obj = {
a : 3,
f2 : f2
}
f1(obj.f2);

原因很简单,f1(obj.f2)obj.f2赋值给了function f1(f2) {...}中的f2(形参),就像上面讲的函数取了一个别名一样,实际执行的就是直接调用,所以应用默认绑定。

显式绑定

显式绑定很好理解,显式绑定让我们可以自定义this的绑定。我们通过使用函数的applycallbind方法,让我们可以自定义this的绑定。

var a = 0;
function f1 () {
var a = 1;
console.log(this.a);
}
//apply方法绑定this apply(对象,参数数组)
f1.apply({a:2}); //输出2

//call方法绑定this call(对象,参数1,参数2,...)
f1.call({a:3}); //输出3

//bind方法绑定this bind(对象,参数1,参数2,...),bind会返回一个函数,需要调用
boundf1 = f1.bind({a:4});
boundf1(); //输出4

但用applycall来进行显示绑定并不能避免隐性丢失的问题。下面有两个方法来解决这个问题。

1.硬绑定

var a = 0;
function f1() {
var a = 1;
console.log(this.a);
}
var bar = function() {
return f1.apply({a:2});
};
setTimeout(bar, 1000);//输出为2

让我们来分析分析这个代码。我们创建了函数bar,这个函数负责返回绑定好thisf1函数,并立即执行它。 这种绑定我们称之为硬绑定。

这种绑定方法会使用在一个i可以重复使用的辅助函数 例如

var a = 0;
function f1() {
var a = 1;
console.log(this.a);
}

function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments);
};
}

var bar = bind(f1,{a:2});
bar();

可以很明显发现这和我们js自带的函数bind方法很像。是的,在ES5中提供了内置的方法Function.prototype.bind。它的用法我再提一次吧。

var a = 0;
function f1 () {
var a = 1;
console.log(this.a);
}
//bind方法绑定this
//bind(对象,参数1,参数2,...),bind会返回一个函数,需要调用
boundf1 = f1.bind({a:2});
boundf1(); //输出2

2.API调用的“上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多内置函数,都提供了一个可选参数,通常被称为“上下文”,其作用和bind方法一样,都是为了防止隐性丢失。

现在来举个例子吧。

function f1(el) {
console.log(el, this.id);
}
var obj = {
id : "awesome"
};
[1,2,3].forEach(f1,obj);
//最后输出的结果为
// 1 'awesome'
// 2 'awesome'
// 3 'awesome'

逗号操作符

在文章开头我们提到了这样一种表达式(0,f1)(),这是逗号操作符的应用,逗号操作符会依次计算所有的表达式,然后返回最后一个表达式的值。这里(0,f1)会先计算0(无实际意义),然后再返回f1,所以最后为f1()

理解了逗号操作符的使用,那如果我们把f1改为obj.f1呢,即(0,obj.f1)(),这时f1中的this绑定的是谁呢?

直接说结论,绑定的是全局对象。(0,obj.f1)()先计算0,然后返回obj.f1即f1函数本身,所以它返回的是一个解绑this的函数,其相当于f1.call(window)——window是全局对象。

下面我们来验证一下吧。

var a = 0;
function f1() {
var a = 1;
console.log(this.a);
}
var obj = {
a : 2,
f1 : f1
};
(0,obj.f1)(); //输出0

完全正确哈哈,注意这种方式不算作隐性丢失哦。

  • 这个操作只是调用了 obj.f1,并没有阻止垃圾回收(GC)。
  • 如果 obj 或 f1 没有其他引用,它们仍然会被正常回收。

如果对其具体的工作流程感兴趣,可以去网上再找些资料。本篇就不讲太详细了。

new 绑定

这是this绑定的最后一条规则。

new绑定通常的形式为:... = new MyClass(参数1,参数2,...)

JavaScript中的new操作符的机制和那些面向类的语言的new操作符有些不一样,因为JavaScript是基于原型的语言(这个也许以后我会谈谈哈哈)。在JavaScript中,“构造函数”仅仅只是你使用new操作符时被调用的函数。

使用new来调用函数,会自动执行以下操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行原型连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

我们现在重点要关注的是第三点。

function f1(a){
this.a = a;
}
var bar = new f1(2);
console.log(bar.a); //输出为2
console.log(f1.a); //输出为undefined

这段代码就可以很明显的看出来new会创建一个新对象bar,并把this绑定到这个bar上,所以才会在bar上创建a这个属性。而原来的f1上则没有a这个属性,所以是undefined

四条规则的优先级

  1. 如果某个调用位置应用了多条规则该怎么办?这时我们就需要知道它们的优先级了。 首先,默认绑定的优先级是最低的。我们先来测试一下它们隐式绑定和显式绑定哪个优先级高吧,这里我偷个懒,就引用一下《你不知道的JavaScript(上卷)》这本书的测试代码
function foo() {  
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

稍微分析一下吧,obj1.foo()obj2.foo()为隐式调用,this分别绑定的为obj1obj2,所以会打印23。接着我们调用了obj1.foo.call(obj2)发现结果输出为obj2中的a属性2,所以这里应用的是显式绑定。

所以显式绑定的优先级是高于隐式绑定的

  1. 再来看看new绑定和隐式绑定的优先级谁更高吧。
function foo(something) {  
this.a = something;
}
var obj1 = {
foo: foo
};
obj1.foo(1);
console.log( obj1.a ); // 1

var bar = new obj1.foo(2);
console.log( obj1.a ); // 1
console.log( bar.a ); // 2

var bar = new obj1.foo(2)这段代码,如果隐式绑定的优先级会大于new绑定,就会在obj1里把属性a赋值为2; 如果new绑定的优先级大于隐式绑定,就会在bar中创建一个属性a,值为2,最后看obj1.abar.a谁输出为2,谁的优先级就更高,很明显bar.a输出为2,所以new绑定的优先级高于隐式绑定的。

所以new调用的优先级要高于隐式调用的优先级

  1. 再来看看new调用和显式调用的优先级谁高谁低吧。

new不能和applycall方法同时使用,但我们可以用bind方法进行硬绑定,再用bind返回的新函数再new一下以此来判断谁的优先级高。

function foo(something) {  
this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

首先硬绑定了obj1,在obj1中创建了a属性,值为2bar接收返回的bind函数。之后new bar并给a赋值为3,用baz来接收new的对象,这时如果baz.a3就说明this应用的绑定规则是new绑定。

所以new绑定的优先级是高于显示调用的优先级的。

现在知道了四种规则,又知道了这四个规则的优先级,我们就能很清晰的判断this的绑定了。

判断this的流程

以后判断this我们可以按以下顺序来判断:

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

     var bar = new foo()  //这里bar为this绑定的对象
  2. 函数是否通过callapply(显式绑定)或者硬绑定(bind)调用?如果是的话,this绑定的是指定的对象。

     var bar = foo.call(obj)  //这里obj为this绑定的对象
  3. 函数是否在某个上下文对象中调用(隐式绑定)如果是的话,this绑定的是那个上下文对象。

     var bar = obj.foo()  //这里obj为this绑定的对象
  4. 如果都不是,则应用默认绑定,this绑定到全局对象上。

     var bar = foo()   //this绑定的为全局对象 

凡事都有例外,还有一些十分特殊的情况不满足上面的四条规则,我们需要单独拎出来记忆。

绑定例外

绑定例外主要有3种。

null导致的绑定意外

var a = 0;  
function f1() {
var a = 1;
console.log(this.a);
}
f1.apply(null); //输出为0

var bar = f1.bind(null);
bar() //输出为0

当我们使用显式绑定(使用apply、call、bind方法)的时候,如果我们显式绑定一个null,就会发现this绑定的不是null而是应用默认绑定,绑定全局对象。这会导致变量全局渗透的问题。

有的人可能会说,那我们不用null来绑定this不就好了吗?但有的时候我们还真不得不使用null来绑定this,下面我来介绍一下什么时候会使用这种情况。

一种常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。类似地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时很好用的。

function f1 (a , b) {  
console.log("a:" + a + ",b:" + b);
}

f1.apply(null,[2,3]) //输出为a:2,b:3

//bind的柯里化
var bar = f1.bind(null,2);
bar(3); //输出为a:2,b:3

现在来简单地来介绍一下柯里化是什么?柯里化是将一个接收多个参数的函数转换为一系列只接受单个参数的函数。这时bindnull的作用就体现出来了。

然而,在apply,call,bind使用null会导致全局溢出,在一些有this的函数中,给这个this绑定null,会让this绑定全局对象。该如何解决这个问题呢?

更安全的this

我们可手动创建一个空的对象,这个空的对象我们称作“DMZ”(demilitarized zoo,非军事区)对象——它是一个空的非委托的对象。

如果我们在想要忽略this绑定时总是传入一个DMZ对象,那就不用担心this会溢出到全局了,这个this绑定的就是DMZ对象。

在JavaScript中创建一个空对象最简单的方法是Object.create(null)——它会返回一个空对象,Object.create(null)Object.create(null){}很像,并不会创建Object.prototype这个委托,所以它比{}“更空”。

var c = 0;
function f1 (a , b) {
this.c = 1;
console.log("a:" + a + ",b:" + b);
}
//创建自己的空对象
var myNull = Object.create(null);

f1.apply(myNull,[2,3]) //输出为a:2,b:3
console.log(c); //输出为0

//bind的柯里化
var bar = f1.bind(myNull,2);
bar(3); //输出为a:2,b:3
console.log(c); //输出为0

可以发现这段代码中,我们创建了自己的空对象通过applybind方法把this绑定到这个空对象了。最后的输出的c0,说明this.c并没有修改全局变量c的值。所以这个方法可以防止全局溢出。

接下来谈谈另外一个绑定的例外吧。

间接引用

有的时候你可能(有意或无意地)创建了一个函数的“间接引用”,在这种情况下,调用这个函数应用默认绑定规则。

var a = 0;
function f1() {
console.log(this.a);
}
var obj1 = {
a : 1,
f1 : f1
};

var obj2 = {
a : 2,
};
obj1.f1(); // 1
(obj2.f1 = obj1.f1)(); // 0

我们来看看这个代码。obj1中有af1属性或方法,a的值为1obj2中只有a属性,值为2。我们先隐式绑定obj1this绑定obj1,最后输出为1,这个我们可以理解。关键是下面这行代码(obj2.f1 = obj1.f1)()obj2中没有f1,所以它在obj2中创建一个f1,然后将obj1中的f1函数赋值给obj2f1,然后执行这个赋值表达式。那为什么输出的是0而不是obj2中的2或者obj1中的1呢? 🤔

其实这和赋值表达式的返回值有关系,因为赋值表达式会返回等号右边的值。 所以(obj2.f1 = obj1.f1)实际上返回的obj1.f1中的f1函数,实际执行的是f1()。所以应用的是默认绑定,this绑定全局对象,结果输出为0

我们继续看绑定的下一个例外。

箭头函数

在ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。

箭头函数和一般的函数不一样,它不是用function来定义的,而是使用被称作“胖箭头”的操作符=>定义的。

定义格式:(参数) => {函数体}

箭头函数不使用this的四条规则,而是继承外层(定义时所在)函数或全局作用域的this的值,this在箭头函数创建时就被确定,且永远不会被改变,new也不行。

var a = 0;
(()=>{
var a = 1;
console.log(this.a); // 结果输出为0
}
)();

很明显该箭头函数外部就是全局作用域,所以继承全局对象的this就是它本身,所以输出为0

再看看如果在其他函数中定义箭头函数this如何绑定

var a = 0;
function f1() {
var a = 1;
(()=>{
var a = 2;
console.log(this.a);
}
)();
}
f1();//输出0

//给f1绑定一个对象
f1.apply({a:3}); // 输出3

可以发现f1内部的箭头函数继承了其外部函数f1this的绑定。所以一开始没给f1绑定this时,f1this绑定的是全局对象,箭头函数的也是全局对象;当给f1this绑定一个对象时,箭头函数的this也绑定该对象。

小结

以上是我的学习分享,希望对你有所帮助。

还有本篇的四条规则只适用于非严格模式,严格模式的this的绑定我日后再出一篇吧,其实只是有点懒😂。

参考书籍

《你所不知道的JavaScript(上卷)》


作者:mrsk
来源:juejin.cn/post/7504237094283526178

0 个评论

要回复文章请先登录注册