本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1.this语法
1.1利用引用标识符绑定this
var name = 'Yuki';
var examCount = 0;
var card = {
name: 'Haruhi',
examCount: 0,
show: function showSum() {
var itSelf = this;
if (itSelf.examCount < 1) {
setTimeout(function sum() {
console.log(itSelf.name);
}, 1000);
}
},
};
card.show(); /* 我们在这里调用对应对象内部的函数方法 Haruhi */
通过var itSelf=this,我们利用闭包和词法作用域的原理,将itSelf设置为一个进行引用的标识符,从而轻松得到了同this的绑定对象(card),无需关心this绑定的过程发生了什么。
首先,itSelf在词法作用域的”力量“之下,将this的绑定指向了card,在外部作用域的调用下,this将忠心耿耿地跟随card不动摇,可能有些同学就会比较迷惑了:
啊这?我要是不对this进行标识符引用呢?会发生什么事?
var name = 'Yuki';
var examCount = 0;
var card = {
name: 'Haruhi',
examCount: 0,
show: function showSum() {
/* var itSelf = this */
if (this.examCount < 1) {
setTimeout(function sum() {
console.log(this.name);
}, 1000);
}
},
};
card.show(); /* Yuki*/
事实上,这就涉及到了隐式绑定丢失的问题,因为setTimeout是JS的内置方法,其执行的上下文是基于全局作用域,当函数引用存在上下文对象时,隐式绑定规则会把函数调用中的this绑定到当前上下文的对象。
show无论是直接在card中先定义还是再添加为引用属性,这个函数严格来说都不属于card对象,card拥有的只不过是该函数的引用,并非函数本身。
利用setTimeout进行执行时,如果不对this进行同向绑定,直接指向了setTimeout所在的全局作用域。
1.2箭头函数里面的this
问题是解决了,但是这玩意拉拉垮垮一大串,有没有点先进的写法呢?
当然有,这就是ES6的大杀器——箭头函数。
show: function showSum() {
if (this.examCount < 1) {
setTimeout(() => console.log(this.name), 1000); //Haruhi
}
},
箭头函数完全摒弃了普通函数绑定this的规则,直接利用当前词法作用域覆盖了this原本的值(调用作用域链?我不干!),这里事实上根本没有什么复杂的内部操作,单纯就是箭头函数的作用下直接”继承“了show函数的this绑定,稳就一个字,谁也别想干扰我。
当然了,有些人(面试官)说:
“啊不不不不,我就不用箭头函数,我也不要用你那个难看的this指定,我要简单!简单利落的写法!”
话音刚落,你摔门而出(×)
只见你沉着了下来,在纸上刷刷刷写出让对方眼前一亮的写法:
var name = 'Yuki';
var examCount = 0;
var card = {
name: 'Haruhi',
examCount: 0,
show: function showSum() {
if (this.examCount < 1) {
setTimeout(
function sum() {
console.log(this.name);
}.bind(this),
1000
);
}
},
};
card.show(); /* Haruhi*/
好家伙,你直接利用bind()来直接套牢了setTimeout!直接将this又绑给了show的引用对象!
(注意,利用bind绑定this,不会立即执行该函数)
面试官:......什么时候来上班?
玩笑归玩笑,利用bind绑定对应的this对象,事实上是最稳妥的一种方法。
关于理解bind的传送门->developer.mozilla.org/zh-CN/docs/…
事实上,我是最推荐第三种写法的,如果你大量使用第二种显示指定this的绑定对象,确实会让代码变得既不优雅也显得混乱。
2.理解this到底指向谁
2.1this指向函数?
this和函数的关系
function iPhone(version) {
console.log('this is iPhone' + version);
this.count++;
}
/* 我们为iphone函数里面的count赋值 */
iPhone.count= 0;
for (var i = 0; i < 17; i++) {
if (i >= 8) {
iPhone(i);
}
}
console.log(iPhone.count); /* --> 输出结果为0 */
我们首先把version和count区分开来,version确实得到了正确的输出,但是作为计数器的count却为0,这不对啊,不是说好了this绑定指向函数自身么?
事实上,我们执行iPhone.count=0时,确实为函数自身添加了一个名为count的属性,但是iPhone函数内部的this.count的this并未指向iPhone函数内部的局部变量count。
this.count的查询流程
-
iPhone函数内部的this.count,它首先是一个隐式赋值的操作(this.count=this.count+1),会基于LHS查询在由里向外一层层查询count。
-
当查到顶层都未查询到count时,他会自动为count在顶层创建一个值为undefined的属性(非严格模式,严格模式下报ReferenceError错误)。
-
随后的this.count++操作直接导致了数值算法的识别错误,返回一个NaN(undefined类型运用于数值运算会返回NaN)。
当然了,聪明的你肯定想到了好方法来解决这个问题啦,那就是:
用什么this!老夫从来都是具名函数名指哪打哪,函数名运用一把梭!
function iPhone(version) {
console.log('this is iPhone' + version);
iPhone.count++;
}
/* 初始化iphone的count */
iPhone.count = 0;
for (var i = 0; i < 17; i++) {
if (i >= 8) {
iPhone(i);
}
}
console.log(iPhone.count); /* 9 */
利用具名函数来指向具体的对象,这下确实是出来了。
但问题是如果我们这么操作,那就又回到了词法作用域的应用范围,而没有利用this完成对应的操作,而且一旦遇到以下存在需要setTimeout利用回调函数的情况,立刻就无效了。
for (var i = 0; i < 17; i++) {
if (i >= 8) {
(function (j) {
setTimeout(function () {
console.log('this is iPhone' + j);
iPhone.count++;
}, 100);
})(i);
}
}
console.log(iPhone.count); /* //报错,iPhone is not defined */
传入setTimeout的回调函数直接就变成了匿名函数,压根没法在函数内部对自身进行再引用,故具名函数的应用,存在较大的局限性。
上述的例子你就算写成行内函数表达式(setTimeout(function iPhone(){......}))也没有用,外部也根本访问不到。
事实上,第二种方法完全是基于词法作用域的理念指导下实现的,众所周知,this的指导思想类似于动态作用域,要遵循函数调用的位置去指向具体的对象。
利用具名函数手动指向对应对象的方法,就是基于函数iPhone的词法作用域去实现调用对象的绝对指向,完全背离了我们用this的宗旨。
说到这里,其实很多读者已经想出了最佳的解决方法。
那就是call()/apply()/bind()。
首先我们要知道一件事情,那就是函数也属于对象,函数可以通过直接设置属性或者通过this设置属性值。。
function iPhone(version) {
console.log('this is iPhone' + version);
this.count++;
}
iPhone.count = 0;
for (var i = 0; i < 17; i++) {
if (i >= 8) {
iPhone.call(iPhone, i);
}
}
console.log(iPhone.count); /* --> 9 */
通过call来保证this指向函数本身,是一种最佳的解决方案。
与此同时,通过这个例子,你可以发现:
this根本就不会指向函数的词法作用域,它的指向取决于函数执行的上下文环境。
下面我们通过一段经典代码来看一下这个大坑,包括笔者在内经常都会遇到这个问题:
function apple() {
var a = 1;
this.orange();
}
function orange() {
console.log(this.a)
}
apple()
你们猜最后打印出来的是啥? 事实上根据RHS查询,最后打印出来的应该a is not defined的ResferenceError类型的报错,但是当前版本的谷歌浏览器会倾向于将其输出为undefined(非严格模式)。
当apple被调用时,是基于全局作用域进行的调用,当它调用内部的orange时,确实函数是基于全局作用域被找到并且调用了,以至于orange内部输出的this指向的是window。
this:你随便定义,我要是一头撞进你的词法作用域里面算我输。
有人可能会觉得,哎?那我要是再在这里利用call绑定apple的this不就行了吗?那我不也可以绑定对应函数的词法作用域?
很遗憾,并不可以,请看代码:
function apple() {
var a = 1;
orange.call(apple);
}
function orange() {
console.log(this.a);
console.log(this);
}
apple();
this.a并没有指向对应函数apple的词法作用域中的属性a,尽管this指向的是函数apple本身,但并不指向apple函数的局部变量,函数内部的局部变量不是函数的属性。
在 JavaScript 中,函数的局部变量和函数的属性是两个不同的概念。
在这个修改后的代码中,this.a = 1; 这行代码将 a 设置为 apple 函数的一个属性。然后,orange.call(this); 这行代码将 orange 函数的 this 设置为 apple,所以 this.a 在 orange 函数中的值是 1。
var a = 1; 这行代码在 apple 函数的作用域内定义了一个局部变量 a。这个变量只能在 apple 函数内部访问,而不能通过 apple.a 或 this.a(当 this 指向 apple 时)访问。
如果你想让 a 成为 apple 函数的一个属性,你需要这样写:
function apple() {
this.a = 1;
orange.call(this);
}
和前面利用call绑定函数本身并传参的打印结果(见下)的区别在于:
前面代码最后打印的是全局作用域下iPhone函数本身的内部值(直接使用词法引用标识符,基于词法作用域查询),而这里使用的是函数内部的this(确实指向函数,但不指向函数的词法作用域),足以看出this并没有追踪对应函数的词法作用域的能力。
console.log(iPhone.count); /* --> 9 */
为什么会这样,只需要记住一句话就够了:
this 只能访问其指向的对象的属性,不能访问对象内部的局部变量。
2.2如何判断this的调用位置
我们必须要明确一个概念,this是类似于动态作用域机制的产物,其产生绑定效果时,是基于运行时绑定,而非编写时绑定,和调用位置没有任何关系,只取决于你的上下文究竟是怎样调用的。
函数被调用时,会创建一个执行上下文,这个执行上下文包括了被调用的位置,函数以何种形式来进行调用,以及对应的传入参数等上述信息。
而this,就是这个记录的一个属性,我们在函数执行的过程中,将会运用到this。
在 JavaScript 中,this 并不指向函数的词法作用域。this 是在运行时基于函数的执行上下文动态绑定的,它可能指向一个对象,也可能指向全局环境,或者是 undefined(在严格模式下)。
词法作用域是在函数定义时确定的,而不是在函数调用时。它是由函数被声明时的位置决定的。词法作用域关注的是变量和块作用域。
相反,this 的值是在函数调用时确定的,它取决于函数是如何被调用的。例如,如果一个函数被作为对象的方法调用,this 将指向那个对象。如果一个函数被作为普通函数调用,this 将指向全局对象(非严格模式)或 undefined(严格模式)。
唯一的例外是箭头函数。箭头函数不绑定自己的 this,它会捕获其所在(即定义的位置)上下文的 this 值。这种行为类似于词法作用域的规则。
如果利用call/bind/apply使得this同向绑定到对应的函数,那也无法利用this追踪到对应函数的词法作用域。
我们在关注this的调用来源时,首要关注的是当前执行函数的前一个调用,你可以将杨宗纬的《洋葱》中的那句“一层一层剥开你的心”作为理解this调用的抽象记忆。
下面我们直接来看一下前面贴的代码的改版,这是一个动态作用域的调用演示案例,举个🌰:
function apple() {
/* 当前调用栈是apple,如果它位于全局作用域,那么当前调用位置就是全局作用域 */
/* 如果它的上一层调用上下文是属于某个函数的执行上下文,它就属于某个函数 */
var a = 1;
orange();
}
function orange() {
/* 当前调用栈属于orange->apple,目前的调用位置被锁定在apple! */
console.log('我在中间这层!');
banana();
}
function banana() {
/* 当前调用顺序是 banana->orange->apple 噢!我的真正“指挥者”是apple! */
console.log('我在最里面那层!');
}
apple();
注意,我们可以看出apple是基于全局作用域来调用的,而函数被追踪出来的调用位置将决定了this的绑定位置。
总结:决定函数this指向的是函数在哪里被调用,this指向的是函数执行位置的上下文动态绑定的。
2.3this的绑定规则
首先,this的绑定规则分为四个大类,分别是默认绑定、隐式绑定、显示绑定和new绑定,这四个绑定规则将使得我们对this的调用思路更加清晰化和明了化。
当然,我们在运用这四个规则时,你必须要自己找到this的“真正”调用位置。
2.3.1默认绑定
下面举个🌰
(非严格模式)
var a = 5;
function apple() {
var a = 1;
console.log(this.a);
}
apple();
你猜结果是多少?
相信经过前面讲解的你已经可以得出结论了:没错!是5!
函数apple在运行的过程中,this.a遵守默认绑定原则,直接对准自身函数的调用位置
下面模拟一下a的查询之路:
- apple里面的a(向this提问):嘿老哥,我是a的影子斥候,你从哪把我调用的我的真身?
- this:稍等,我问一下编译器。
- 编译器:滚滚滚,你这种运行时生成的代码别问我(this在函数运行时生成),去问JS引擎,我是在代码执行前进行静态编译的!
- this:呃...好吧,那引擎大哥,我带来的a到底是谁派来的啊?
- JS引擎:咳咳咳等会,你是执行栈里面的还是调用栈的...嗯嗯...是执行栈里面的兄弟啊,我看看....哦找到了,你的老大apple函数是在全局作用域调用的,在那个作用域里面有一个属性a,去找吧。
- this:好的老哥!我这就去告诉a!嘿!a!我们去全局作用域里面找!
- a:好嘞,我这就去找...嗯,我找到我的真身了!它的值是5!
此时你会发现,apple函数的调用压根没用任何修饰符(XXX.apple()),所以会被强制应用默认绑定,其他绑定原则不可生效。
一旦使用默认绑定,那就基于最终调用上下文的位置,决定了其this指向。
再举一个小例子,了解一下默认绑定原则的间接引用可能会引起的问题:
function fruit() {
console.log(this.a);
}
var a = 3;
var apple = { a: 2, fruit: fruit };
var banana = { a: 5 };
apple.fruit(); //2
banana.fruit = apple.fruit;
banana.fruit(); //5
我们可以看到,banana.fruit调用的是fruit函数本身,但由于其修饰符为banana,所以调用位置基于banana的对象上下文去查找a,这里会直接使用默认绑定原则寻找this.a。
当然了,你有没有发现开头我写的那个“(非严格模式)”呢?
在严格模式下,全局作用域不可应用于默认绑定规则,最终this是谁也找不到,报一个TypeError:this is undefined的错误。
var a = 5;
function apple() {
"use strict"
var a = 1;
console.log(this.a);
}
apple();
注意,TypeError代表的是属性被找到了,但是将属性运用于后续的操作,导致属性类型和操作方式发生了冲突,从而产生了 TypeError 。
决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式,如果函数体处于严格模式,this会被绑定到undefined,否则会被绑定到全局对象。
如果你在全局作用域下,基于严格模式用立即执行表达式来在内部引用apple(),事实上是可以找到全局作用域中的a值的,当然这个没有太大的讨论意义。
2.3.2 隐式绑定
在这里,我们要基于前面说的默认绑定里面提到的“apple函数的调用压根没用任何修饰符”,继续讲讲隐式绑定。
你还记得开头的第三段代码吧?
var name = 'Yuki';
var examCount = 0;
var card = {
name: 'Haruhi',
examCount: 0,
show: function showSum() {
if (this.examCount < 1) {
setTimeout(
function sum() {
console.log(this.name);
}.bind(this),
1000
);
}
},
};
card.show(); /* Haruhi*/
来,我们看一下show()的声明方式,你还记不记得我在讲这段代码的时候,刻意提到过“show无论是直接在card中先定义还是再添加为引用属性,这个函数严格来说都不属于card对象,card拥有的只不过是该函数的引用,并非函数本身。”
同学们!大家要注意看这段话!
尽管show不属于card对象,但是在全局作用域使用card.show()来引用函数时,调用位置正是card对象!
我们不要混淆概念,因为很多同学心里面都在默认“啊那个函数也属于对象啊,啊那个函数也属于对象啊”,师父你先别念了,听我下面要说的:
对象内部可以包含对函数的引用,且this调用位置存在上下文对象!—— 这是最关键的分水岭。
同时笔者在某种程度上认为,对象不构成作用域,而函数是会构成作用域的(存疑,大家先别确定这句话,只是笔者在研究JS的时候自由心证的结论)。
function sum里面的this在查询自身归属时会发现,show()方法存在card上下文对象的引用,当函数引用存在上下文对象(card)时,隐式绑定会把函数调用内部的this直接绑定在这个上下文对象,故this.name===card.name。
同时,对象属性的引用规则只有在最后一层调用位置中有效,你可以武断的理解为:带对象引用的函数最后在的引用符是哪个上下文对象,那它的this就指向这个上下文对象!
function mac(){
console.log(this.a);
}
var apple={
a:'M1 PRO',
mac:mac
}
var intel={
a:"11900k",
apple:apple
}
intel.apple.mac() //输出为M1 PRO!哎嘿!我还是用了自家芯片!
我们可以看到,mac的最终调用位置为apple,this.a自然输出的是"M1 PRO"。
大坑:隐式绑定的丢失
实际上,这个隐式绑定还是非常不靠谱的。
上面说到的隐式函数绑定看起来很稳,但事实上隐式绑定中的绑定对象是个非常不靠谱的主儿,借此套用一句中学教科书的名言:
“我的附庸的附庸不是我的附庸”。
看这!
function pineapple() {
console.log(this.a);
}
var apple = {
a: '我是苹果!',
pineapple: pineapple,
};
function fruit(mine){
//实际调用的是pineapple本身
mine();
}
fruit(apple.pineapple);
var a = '我是全局作用域的数据';
fruit(); //调用位置在全局作用域,输出 '我是全局作用域的数据'
虽然fruit调用时的参数apple.pineapple,看起来像是apple对象内部的pineapple函数基于mine()而被调用,但实际上被调用的根本不是apple内部的pineapple,它是基于全局作用域直接引用了pineapple本身,其在调用的时候没有带任何修饰符,直接在非严格模式下应用了默认绑定。
apple:没想到吧,你要是直接调用我的pineapple引用,那压根和我没关系。
全局作用域下,假设没有a这个同名属性,同样也无法生成输出结果,因为当前执行上下文根本没有这个值。
我们需要记住一件事情,参数的传递事实上是一种隐式赋值(LHS查询)。
隐式赋值时,被赋值的参数会根据调用函数实际所在执行上下文的位置(上述例子中的apple.pineapple实际上调用的是pineapple,而执行调用的fruit位于全局作用域),来决定this的调用。
即便我们将pineapple写入apple对象,我们最终看的也是调用上下文fruit的实际调用位置来决定。 pineapple写入了apple对象,pineapple实际上也不属于apple对象,apple里面的Pineapple保存的是实际的函数对象的一个内存地址,指向对应函数对象的堆栈空间保存的数据。
就算你使用ES6的简介语法声明(不需要function,直接在对象内定义pineapple(){...})也是一样的,前者只不过是利用匿名函数赋值的一种语法糖,没有改变JS的规则。
总结一下:
1.无论是基于对象调用对象内部的函数,还是通过声明一个属性,以此属性引用对象内部的指向函数属性“间接”调用,实际上都看最终调用者的调用位置。
2.对象引用函数只是函数引用而非函数本身,但我们确实可以通过上下文对象引用函数的方法将this绑定到上下文对象上面去,从而读取对象上面的值。
接下来,我们可以来利用显示绑定聊聊如何修复这个问题。
2.3.3 显式绑定
如果我们想在一个对象上强制调用某个函数,令其this指向本对象,而不想再用上述绕弯弯一般的“对象内部包含指定函数引用“的方法,我们该怎么做呢?
相信很多人已经想到了,那就是
1.function.call(thisArg, arg1, arg2, ...)
2.function.apply(thisArg, [argsArray])
3.function.bind(thisArg, arg1,arg2,...)
call和apply的第一个参数指定是一个对象,用于指定this,将调用函数指向对应的对象。第二个参数为携带参数,用来传递函数的额外参数。
这,就是显式绑定。
var a = '不!你不是!';
var apple = {
a: '我是凤梨',
};
function pineapple() {
console.log(this.a);
}
pineapple.call(apple); //输出结果->'我是凤梨'
如果我们利用闭包结合显式绑定的方式来绑定对应的上下文对象,可以牢牢地将this绑定到对应的上下文对象,无论以后怎么改都不会变更绑定后的this绑定主体,这种方法可以称之为显式绑定的硬绑定。
function pineapple() {
console.log(this.a);
}
var apple = {
a: '我是凤梨',
};
var a = '不!你不是!';
var fruit=function(){
pineapple.call(apple)
}
setTimeout(fruit, 100); //'我是凤梨'
fruit.call(window); //'我是凤梨'
无论我们从何处调用fruit或者更改fruit的指向,它都会牢牢地在apple上调用pineapple函数,这就是显式硬绑定,和bind绑定效果是等效的(但是两者代码架构完全不一样,只是效果一致)。
我们可以通过显式硬绑定原理创建一个简单的组合辅助函数compose,来进一步了解显式硬绑定的原理:
function apple(num) {
console.log(this.a, num);
return this.a + num;
}
function compose(fn, obj) {
return function () {
return fn.apply(obj, arguments);
};
}
var market = {
a: 1
};
var superMarket = compose(apple, market);
var getSums = superMarket(5)
console.log(getSums);
-
我们通过superMarket调用compose,并将apple函数作为第一个参数传入,market对象作为第二个参数传入。
-
现在我们来看compose,在得到调用后返回一个匿名函数
function () { return fn.apply(obj, arguments)},该匿名函数符合闭包的创建条件,执行完成后,compose函数被回收,其apple函数和market对象和匿名函数的作用域依然被保留,并赋值给superMarket。 -
随后通过getSums调用superMaket,并且隐式赋值传入一个参数5。
-
毫无疑问,apply函数中的arguments接收了参数5(LHS查询),返回一个
apple.apply(market,5)的结果,apply函数会自动运行,打印出1 5这样的结果。 -
随后
fn.apply(obj,arguments)被回收,返回一个6的结果,apple函数也被回收(返回基本数据类型不会构成闭包),最终返回的结果6由getSums接收。 -
打印getSums的值。
核心代码:
return function () {
return fn.apply(obj, arguments);
}; //显式硬绑定
通过这个组合辅助函数,我们可以逐个生成对应的闭包函数,在apply函数自动执行后,superMarket(5)也会被回收,达到了打印携带参数值和返回结果的效果。
而apple函数里面的this.a直接指向了我们前面说的显式硬绑定market对象,牢牢地在对应的对象上调用对应方法,而不受外界影响。
2.3.4 new绑定
在讲解new绑定开始之前,我们首先要对构造函数有一定的了解。
JavaScript中的构造函数在使用new操作符调用函数时,和其他语言的操作截然不同!
JS中使用new调用的函数只是普通函数,而并非是一个真正的类,事实上JS中根本不存在真正的类,通过new调用的函数并不会真正去实例化生成某一个类。
function pineapple(num) {
this.num = num;
}
var apple = new pineapple(5);
console.log(apple.num); //5
用new来调用函数在ES5的语言规范中,概括来说其定义是“构造函数调用”,JS中使用new来调用函数,会执行以下操作:
- 根据被调用的函数从而构造出一个全新的新对象。
- 这个全新生成的对象的[[prototype]]属性会自动关联到new调用的构造函数
- 生成的新对象会绑定函数调用中的this,也就是new调用的构造函数中的this会在构造完成后指向新对象。
- 若是碰到new调用的函数内部没有return一个指定的对象,那么new表达式的函数调用也会返回给这个新生成的对象。
这就是最后一种this绑定——new绑定。
2.3.5 绑定的优先级顺序
了解各种绑定的优先级有利于我们对项目的功能代码进行迭代,也能快速排查错误。
我们从前面的例子大概可以了解到,默认绑定的优先级处于最低级。
那么显式绑定、隐式绑定和new绑定的this绑定优先级哪个更高呢?
function showPriority(num) {
this.a = num;
}
//定义一个对象,内部指定了showPriority的函数引用
var apple = {
showPriority: showPriority,
};
var banana = {};
//隐式绑定
apple.showPriority(5);
console.log(apple.a); //5
//显式绑定,变更绑定对象为banana并携带参数2,根据输出结果可以看出显式绑定优先级高于隐式绑定
apple.showPriority.call(banana, 2);
console.log(banana.a); //2
//new绑定
var orange=new apple.showPriority(4);
//注意!apple对象的a没有被改变
console.log(apple.a); //5
//new绑定优先级高于隐式绑定,orange的内部属性a的值为new绑定时的携带参数4
console.log(orange.a); //4
我们可以看到,利用new绑定构造了apple.showPriority()之后,apple对象的属性a并未受到影响,只是构造了一个全新的对象并赋值给orange,但是可以看出new绑定的优先级是高于隐式绑定的。
我们目前可以得出一个结论,new绑定和显式绑定优先级均高于隐式绑定。
接下来我们继续比较new绑定和显式绑定的优先级:
首先我们需要明确一点:new和call/apply不能组合使用,故不可直接比较new绑定和显式绑定的优先级,但我们可以利用bind来确定前述两者的优先级。
bind和组合辅助函数compose虽然运行运行效果极其相似,但是实际上代码架构是完全不一样的,请看以下代码:
function apple(params){
this.a=params
}
var obj={}
var banana=apple.bind(obj);
banana(2);
console.log(obj.a); //2
var orange=new banana(3)
console.log(obj.a); //2
console.log(orange.a); //3
你会发现bind绑定被new绑定直接“劫持”了banana()到obj的this指向,使apple应用函数里面的this指向了新生成的orange对象!
我们前面在讲解显式硬绑定的时候,执行显式硬绑定的函数可是无论如何都不会被变更this绑定对象的。
这是因为如果我们使用类似显式硬绑定效果的bind绑定进行绑定,new绑定是可以更改bind绑定的this绑定对象。
那这时你可能会问:你刚刚不是说bind绑定效果等同于你上面写的显式硬绑定函数吗?为什么显式绑定不能用,而只能用new绑定结合bind绑定?那如果我就要结合new绑定用显式硬绑定呢?
很遗憾,请务必记得,new绑定和call/apply是不能一起使用的,我们前面自定义的显式硬绑定polyfill最终生效还是要靠apply/call来整体实现类似bind绑定的效果,因为new调用的返回值并非函数,只是单纯的基本数据类型,导致最后生成的只有一个空对象。
function apple(num) {
return (this.a = num);
}
function compose(fn, obj) {
return function () {
return fn.apply(obj, arguments);
};
}
var market = { a: 1 };
var superMarket = compose(apple, market);
var getSums = superMarket(5);
console.log(getSums); //5
console.log(market.a); //5
var newBind = new superMarket(3); //本质上是 new fn.apply(obj,arguments)
console.log(market.a); //3
console.log(newBind); //{} 针对数字3进行new绑定,只会默认返回一个空对象
这就涉及到一个关键的知识点:
当你使用 new 关键字调用一个经过 bind 绑定的函数时,会发生以下几件事情:
- 创建一个新的空对象。
- 这个新对象的
[[Prototype]]属性会被设置为绑定函数的prototype对象。 - 绑定函数会以这个新对象作为
this的上下文被调用。
function Fruit(name) {
this.name = name;
}
let Apple = Fruit.bind(null, 'Apple');
let apple = new Apple();
console.log(apple.name); // 输出 "Apple"
在这个例子中,Apple 是 Fruit 绑定了参数 'Apple' 的结果。当我们使用 new 调用 Apple 时,this 会被绑定到新创建的对象,而 bind 的第一个参数 null 会被忽略。所以,新创建的对象的 name 属性会被设置为 'Apple'。
1.new修改this的时候,会判断bind函数是否被new调用,如果是的话就用新创建的this替换bind绑定的this,bind只不过是把第一个参数用于绑定this,其他的传递参数直接传递应用该绑定函数的下层函数,可以理解为一种函数的柯里化。
2.new绑定结合bind绑定使用,不会修改bind绑定原有的绑定对象,因为它会生成一个新的对象,将全新的赋值和生成的对象交给接收属性。
因为我们首先通过bind绑定的接收函数Fruit是已经被定义了,但并没有被调用,只是保留了携带参数,整个绑定你可以理解为实现的是默认绑定原则,根据apple的创建位置直接绑定到了全局对象。
结论
new绑定>显式绑定(包括了类显式硬绑定效果的bind绑定)>隐式绑定>默认绑定。
3.进阶理论
如果我们选用null和undefined来作用this的绑定对象,以此来应用call,apply,bind,实际上调用的函数还是会默认生效的,只不过对应的this会被自动忽略,最终应用的依然是默认绑定原则。
但是我们非常不推荐使用这种方法,如果按照上述的法则进行绑定,根据默认绑定规则会直接把this指定为全局对象(window),这是非常危险的操作。
function fruit(a, b) {
this.num = a + b;
console.log(this.num);
}
var apple = fruit.call(null, ...[1,2]); //3
我们可以看到,如果按照上述方法来进行null调用,会直接导致this绑定依照默认绑定规则将this绑定到了全局作用域。
3.1 利用空非委托对象指定一个“空”的this
事实上,根据上述的利用null来生成一个空对象是不保险的。
我们存在一种安全的改进方法,那就是通过一个空的非委托对象来替换null,作为一个暂时提供“跳板”作用的空对象,但是其空对象内部不会存在[[prototype]]原型链。
function fruit(a, b) {
this.num = a + b;
console.log(this.num); //3
}
var emptyZone = Object.create(null);
var apple = fruit.call(emptyZone, ...[1, 2]);
console.log(this.num); //undefined 全局作用域上并没有num
这使得this绑定不会遵循默认绑定原则直接被“丢”到全局作用域,防止全局作用域修改对象内部值,可以说是通过建立一个这样的“缓冲区”隔绝了冲突风险,还能随时供应new绑定来调用。
3.2 利用软绑定“改造”显式硬绑定
我们通过显式硬绑定来将this牢牢地绑定到指定的对象,尽管我们可以通过bind绑定(类显式硬绑定)结合new绑定的方式来更改this绑定指向,但是终究还是不够灵活。
下面,我们通过《你不知道的JavaScript》上册来了解一下软绑定,笔者已经进行了细致的讲解和标注:
if (!Function.prototype.softBind) {
Function.prototype.softBind = function (obj) {
var fn = this;
var curried = [].slice.call(arguments, 1);
var bound = function () {
return fn.apply(
!this || this === (window || global) ? obj : this,
curried.concat.apply(curried, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
};
}
下面我们来根据上述代码逐个讲解:
-
var fn = this;:这行代码获取了调用softBind方法的原始函数。 -
var curried = [].slice.call(arguments, 1);:这行代码获取了传递给softBind方法的额外参数(除了第一个参数之外的所有参数)。 -
var bound = function () {...}:这行代码定义了一个新的函数。这个新函数在调用时会调用原始函数,并使用特定的this值和参数。!this || this === (window || global) ? obj : this:这行代码决定了新函数的this值。如果新函数的this值是undefined、null或全局对象,那么this值将被设置为softBind方法的第一个参数。否则,this值将保持不变。curried.concat.apply(curried, arguments):这行代码获取了新函数的参数,并将它们添加到curried数组的末尾。这样,新函数在调用时会使用softBind方法的额外参数和自己的参数。
-
bound.prototype = Object.create(fn.prototype);:这行代码将新函数的prototype属性设置为原始函数的prototype属性的一个新实例。这样,新函数可以继承原始函数的方法。 -
return bound;:这行代码返回新函数。
总的来说,softBind 方法允许你创建一个新的函数,这个新函数在调用时会使用特定的 this 值和参数。但是,与 bind 方法不同,softBind 方法允许你在调用新函数时覆盖 this 值(除非 this 值是 undefined、null 或全局对象)。
我们直接来看例子:
function foo() {
console.log('name:' + this.name);
}
var obj = { name: 'obj' },
obj2 = { name: 'obj2' },
obj3 = { name: 'obj3' };
var fooOBJ = foo.softBind(obj);
fooOBJ(); //name:obj
obj2.foo = foo.softBind(obj);
obj2.foo(); //name:obj2 -->this绑定被变更到obj2
fooOBJ.call(obj3); //name:obj3 -->this绑定被变更到obj3
setTimeout(obj2.foo, 10);
//name:obj->应用了全局对象,因为我们在这里并没有设置接收的调用函数
可以看到,我们成功的“改造”了显式硬绑定函数,令其this绑定可以更改到指定的对象。
3.3 真正的“扫地僧”->箭头函数
我们是否还记得一开始的“开胃小菜”,那就是结合箭头函数来绑定this?
在这段代码里面,this死死的指向了show函数的调用对象,任凭谁也无法改变。
show: function showSum() {
if (this.examCount < 1) {
setTimeout(() => console.log(this.name), 1000);
}
},
首先,我们需要明确一点定义:箭头函数并没有使用function关键字来定义,它不适用于上述的this的四大绑定,只认一个死理——“外层作用域的this是谁,俺的this就是谁!”
我们改造一下上述代码:
function showCum() {
/* 返回箭头函数 */
return () => {
/* this继承showCum的调用位置 */
console.log(this.a);
};
}
var apple = {
a: 3,
};
var banana = {
a: 4,
};
var fruit = showCum.call(apple);
fruit(); //3
fruit.call(banana); //3 哦豁!改不了!
你会发现,showCum函数内部的箭头函数自从一开始绑定了apple函数的this调用以后,引用了apple对象的fruit的this也会被牢牢绑定到apple对象上面,任凭如何改变this指向,都没有任何作用。
其实箭头函数应用的正是词法作用域,它没有应用任何this绑定方法,也不允许任何this绑定方法更改其this指向,我们通过下一段代码更深入窥探箭头函数绑定this的实现原理:
function showCum() {
var self = this;
return function () {
console.log(self.a);
};
}
var apple = {
a: 3,
};
var banana = {
a: 4,
};
var fruit = showCum.call(apple);
fruit();
fruit.call(banana);
我们在showCum被回调函数fruit所调用时,showCum已经将this绑定指向了apple对象,也就意味着,在此处我们手动绑定了self的this已经指向了apple对象。
其内部的匿名函数function () { console.log(self.a); };被返回,在这里其实产生了一个闭包,也就是self作为showCum的内部词法作用域被保留,在showCum被调用完毕回收后,self和内部匿名函数并没有被回收,而是返回给了fruit。
在fruit()执行后,该内部匿名函数和self被推进了执行栈进行执行,在执行完毕后才被回收。
再看最后一行代码fruit.call(banana);,虽然此时再次执行了call()绑定和回调,但是已经“为时已晚”,此时的self早已经被绑定在了apple身上,就算再执行了一次this绑定指向,self也早已被手动指向了apple对象。
总结
-
this四大绑定的优先级排序为:new绑定>显式绑定(包括了类显式硬绑定效果的bind绑定)>隐式绑定>默认绑定。而箭头函数不受this绑定的影响。
-
new调用会自动生成一个全新的对象,并将this绑定指向新创建的全新对象。
-
上下文对象的调用,一定要确定其修饰符的最终指向,谁最后调用了this,那this就指向谁。
-
箭头函数会继承外层函数调用的this绑定,原理可见2.4.2.
-
隐式绑定中的函数引用有上下文对象时,隐式绑定会将函数中的this绑定到这个上下文对象,此时这个上下文对象拥有该函数的引用而非函数本身。
-
this不指向函数的词法作用域。
-
在new绑定结合bind绑定时,可利用函数的柯里化方法利用bind预设部分参数,再利用new初始化指定对应的绑定对象。
完毕!希望大家可以通过本篇,彻底精通this的原理~