- 原文地址:Let’s settle ‘this’ — Part Two
- 原文做者:Nash Vail
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:geniusq1981
- 校对者:Moonliujk、coconilu
嗨!欢迎来到让咱们一块儿解决“this”难题的第二部分,咱们试图揭开 JavaScript 中最难让人理解的一部份内容 - “this”关键字的神秘面纱。若是您尚未读过 第一部分,你须要先把它读一下。在第一部分中,咱们经过 15 个示例介绍了默认绑定规则和隐式绑定规则。咱们了解了函数内部的“this”如何随着函数调用方式的不一样而发生改变。最后,咱们也介绍了箭头函数以及它是如何进行词法绑定。我但愿你能记住这一切。前端
在这一部分咱们将讨论两个新规则,从 new 绑定开始,咱们将深刻地分析这一切是如何工做的。接下来,咱们将介绍显式绑定以及如何经过 call(...),apply(...) 和 bind(...) 方法将任意对象绑定到函数内部的“this”上。android
让咱们接着以前的内容继续。你的任务仍是同样,继续猜一下控制台的输出内容是什么。还记得 WGL 吗?ios
不过,在深刻以前,先让咱们经过一个例子来热热身。git
function foo() {}
foo.a = 2;
foo.bar = {
b: 3,
c: function() {
console.log(this);
}
}
foo.bar.c();
复制代码
我知道,如今你可能会想“到底发生了什么?为何在这里将属性分配给函数?这不会致使错误吗?”好吧,首先,这不会致使错误。JavaScript 中的每一个函数也都是一个对象。就像其余普通的对象同样,你也能够为函数指定属性!程序员
接下来,让咱们弄清楚控制台会输出什么。若是您注意下,你会发现隐式绑定在此处起做用。c 调用以前的对象是 bar,对吗?所以 c 中的“this”指向的是 bar,所以 bar 被输出到控制台中。github
经过这个示例,你能够知道,JavaScript 中的函数也是对象,就像任何其余对象同样,它们能够被赋予属性。后端
function foo() {
console.log(this);
}
new foo();
复制代码
那么,输出什么?仍是根本没有输出?数组
正确答案是一个空对象。是的,不是 a,也不是 foo,只是一个空对象。让咱们看看它是如何工做的。app
首先要注意,函数 如何 被调用。它不是一个独立调用,它的前面也没有对象引用。它的前面只有一个 new。在 Javascript 中能够经过 new 关键字来引入任意函数。当这样作的时候,使 new 引入一个函数时,大体会发生四件事情,其中两个是,函数
第二点正是你执行上面的代码时控制台输出一个空对象的缘由。你可能会问“这能有什么用?”。咱们会发现这里有些小争议。
function foo(id, name) {
this.id = id;
this.name = name;
}
foo.prototype.print = function() {
console.log( this.id, this.name );
};
var a = new foo(1, ‘A’);
var b = new foo(2, ‘B’);
a.print();
b.print();
复制代码
直观地说,在这个例子中很容易就能猜到控制台上输出什么,可是从技术角度你知道真正的原理吗?让咱们来看看。
来回顾一下,当使用 new 关键字调用函数时,会发生四个事件。
在前面的例子中咱们已经验证了前两个事情,这就是咱们会在控制台中输出空对象的缘由。先忘掉第三点,让咱们聚焦在第四点上。没有什么能够阻止函数的执行,除了函数内部的“this”是新建立的空对象以外,传参后函数的执行过程与其余正常的 Javascript 函数同样。所以,这个例子中的 foo,在它里面咱们执行相似 this.id=id 的操做时,咱们其实是将属性分配给了在调用函数时绑定到“this”上的新建立的空对象。再读一遍这句话。一旦函数执行完成,就会返回这个刚被建立的对象。因为在上面的示例中咱们为返回的对象分配了 id 和 name 属性,因此这个返回的对象也会拥有这些属性。而后咱们能够将返回的对象赋值给咱们想要的任何变量,就像咱们上面示例中的 a 和 b。
每一个使用 new 关键字的函数调用都会建立一个全新的空对象,在函数内部配置对象的参数属性 _(this.propName = …) 在函数执行完毕后返回这个对象。
var a = {
id: 1,
name: ‘A’
};
var b = {
id: 2,
name: ‘B’
};
复制代码
太棒了!咱们刚刚学会了建立对象的新方法。可是 a 和 b 有一些共同点,它们都是 原型链指向 foo 的原型对象(事件 4),所以能够访问它们的属性(变量,函数等等)。正由于如此,咱们能够调用 a.print() 和 b.print(),由于 print 是咱们在 foo 原型链上建立的函数。快速的问一个问题,当我调用 a.print() 时会发生什么绑定?若是你说发生了隐性绑定,那你就答对了。所以,在调用 a.print() 时,print 里面的“this”指向的就是 a,而且控制台上首先输出的是 1,A,一样当咱们调用 b.print() 时,会输出 2,B。
function foo(id, name) {
this.id = id;
this.name = name;
return {
message: ‘Got you!’
};
}
foo.prototype.print = function() {
console.log( this.id, this.name );
};
var a = new foo(1, ‘A’);
var b = new foo(2, ‘B’);
console.log( a );
console.log( b );
复制代码
几乎与上一个示例中的代码彻底相同,除了请注意,foo 函数如今返回的是一个对象。好吧,让咱们返回上一个例子,重读一下第四点,怎么样?注意加粗的内容了吗?当使用 new 关键字调用函数时,在执行结束时将返回新建立的对象,除非你返回自定义对象,就像咱们在这个示例中所作的这样。
因此?输出的什么?很明显,它返回自定义对象,具备 message 属性的这个对象会在控制台中输出,输出两次。如此容易就打破了整个结构,是否是?只返回了一个没有意义的对象,一切就彻底改变了。此外,你如今没法调用 a.print() 或 b.print(),由于 a 和 b 被分配了返回的对象,但返回的对象没有连接到 foo 的原型链。
但等一下,若是不返回一个对象,咱们返回好比 'abc'、数字、布尔值、函数、nullundefined 或是数组,结果会怎样?事实证实,构造对象是否会改变取决于你返回的内容。看看下面的模式?
return {}; // 改变
return function() {}; // 改变
return new Number(3); // 改变
return [1, 2, 3]; // 改变
return null; // 不改变
return undefined; // 不改变
return ‘Hello’; // 不改变
return 3; // 不改变
...
复制代码
为何会这样呢,这就是另一篇文章的主题了。个人意思是咱们已经离题有点远了,这个例子与“this”绑定没太大关系,对吗?
在 Javascript 中,从好久以前就开始经过使用 new 关键字绑定来建立完整的对象(也许是一种误用),以此来伪造传统的类。实际上,在 JavaScript 中没有类的概念,ES2015 中新的 class 语法只是一个语法。在它的后面仍是使用 new 绑定,没有任何变化。我一点都不关心你是否使用 new 绑定伪造类,只要你的程序工做正常,代码是可扩展,可读和可维护的,就没有问题。可是,因为 new 绑定带来的不稳定性,你如何可以确保全部代码包都拥有可扩展,可读和可维护的代码呢?
可能这里还涉及不少内容。若是你还有点迷茫,你应该再从新阅读一下。重要的是若是你了解了 new 绑定的工做原理,可能永远都不会再使用它 :)。
不开玩笑,让咱们继续。
思考如下的代码。不用猜想这个例子会输出什么,咱们将从下个例子开始继续“猜谜游戏” :)。
var expenses = {
data: [1, 2, 3, 4, 5],
total: function(earnings) {
return this.data.reduce( (prev, cur) => prev + cur ) - (earnings || 0);
}
};
var rents = {
data: [1, 2, 3, 4]
};
复制代码
expenses 对象具备 data 和 total 两个属性。data 包含一些数字,而 total 是一个函数,它将 earnings 做为输入参数并返回 data 中全部数字的总和减去 earnings。很是直观。
如今看一下 rents,就像 expenses 同样,它也有 data 属性。这样说,出于某种缘由,这只是个假设,你想基于 rent 的 data 数组运行 total 函数,由于咱们是优秀的程序员,咱们不喜欢重复工做。咱们绝对没法调用 rents.total(),也没法把 rents 的“this”隐式绑定为 total,由于 rents.total() 是一个无效的调用,由于 rents 没有名为 total 的属性。如今有没有一种方法能够将 rents 的“this”绑定为 total 函数。好吧,猜猜是什么?是有的,请容许我介绍 call() 和 apply()。
你能够看到 call 和 apply 作了一样的事情,它们容许你将你想要的对象绑定到你想要的功能上。这意味着我能够作到这一点……
console.log( expenses.total.call(rents) ); // 10
复制代码
还有这个。
console.log( expenses.total.apply(rents) ); // 10
复制代码
这很棒!上面的两行代码都会致使 total 函数被调用,而内部的“this”被绑定为 rents 对象。call 和 apply 两个方法就“this”绑定而言,只有传递参数的方式不一样。
注意,total 函数有一个参数 earnings,让咱们传一下参数试试。
console.log( expenses.total.call(rents, 10) ); // 0 正常!
console.log( expenses.total.apply(rents, 10) ); // 报错
复制代码
使用 call 给目标函数(在咱们的例子中是 total )传递参数很简单,像给其余普通函数传递参数同样,你只需传入一个由逗号隔开的参数列表 .call(customThis, arg1, arg2, arg3…)。在上面的代码咱们传入了 10 做为 earnings 参数,一切正常。
而 apply 要求你将参数传递给目标函数(在咱们的例子中是 total)时,将参数包装在一个数组里 .apply(customThis,[arg1,arg2,arg3 ...]) 你应该注意到了,上面的代码中咱们没有这样传入参数,因此会发生错误。把参数封装成一个数组,而后再传入,就不会报错了。就像下面这样。
console.log( expenses.total.apply(rents, [10]) ); // 0 正常!
复制代码
我过去曾经总结了一个助记符就是经过上面说的这点差异来记住 call 和 apply 之间的区别的。A 表明 apply ,A 也表明 array !因此经过 apply 把参数传给目标函数时,须要把参数封装成 array 。这只是一个简单的小助记符,但它确实颇有用。
如今若是咱们传入一个数字,或一个字符串,或一个布尔值,或 null/undefined,而不是传入一个对象来调用 call,apply 和 bind (接下来讨论)。那样会发生什么?没有什么特别,好比你给“this”传入数字 2, 它在对象内被封装成对象形式 new Number(2) ,一样若是你传入一个字符串,它会变成 new String(...) ,布尔值会变成 new Boolean(...) 等等,这个新对象,不论是字符,仍是数字或是布尔值都被绑定到被调用函数的“this”。传入 null 和 undefined 的结果会有点不一样。若是调用函数时为“this”传入 null 或 undefined ,那它就好像进行了默认绑定同样,那意味着全局对象被绑定在被调用函数的“this”上。
还有另外一种方法将'this'绑定到一个函数,此次经过一个方法名叫,等等,bind!
让咱们看看你是否能够解决这个问题。下面的示例会输出什么?
var expenses = {
data: [1, 2, 3, 4, 5],
total: function(earnings) {
return this.data.reduce( (prev, cur) => prev + cur ) - (earnings || 0);
}
};
var rents = {
data: [1, 2, 3, 4]
};
var rentsTotal = expenses.total.bind(rents);
console.log(rentsTotal());
console.log(rentsTotal(10));
复制代码
这个例子的答案是 10 后跟着输出 0。注意 rents 对象声明下面发生了什么。咱们从函数 expenses.total 建立一个新函数 rentsTotal 。这里 bind 建立一个新函数,当这个函数被调用时,它的“this”关键字设置为提供的值(在咱们的例子中是 rents )。所以,当咱们调用 rentsTotal() 时,虽然它是一个独立的调用,但它的“this”已指向了 rents ,而默认绑定没法覆盖它。此次调用会在控制台输入 10。
在下一行中,使用参数(10)调用 rentsTotal 与使用相同的参数(10)调用 expenses.total 彻底相同,它只是“this”中的值不一样。此次调用的结果为 0。
另外,你也可使用 bind 绑定参数给目标函数(在咱们的例子中是 expenses.total)。思考下这个。
var rentsTotal = expenses.total.bind(rents, 10);
console.log(rentsTotal());
复制代码
你认为控制台输出什么?固然是 0,由于 10 已经过 bind 绑定到目标函数(expenses.total)做为 earnings 参数。
让咱们看一个例子,它能够说明 bind 生命周期。
// HTML
<button id=”button”>Hello</button>
// JavaScript
var myButton = {
elem: document.getElementById(‘button’),
buttonName: ‘My Precious Button’,
init: function() {
this.elem.addEventListener(‘click’, this.onClick);
},
onClick: function() {
console.log(this.buttonName);
}
};
myButton.init();
复制代码
咱们已经在 HTML 中建立了一个按钮,而后咱们在 Javascript 代码中,将这个按钮定义为 myButton 。注意,在 init 中,咱们还为按钮上添加了一个鼠标点击的事件监听。你如今的问题是当点击按钮的时候,控制台会输出什么?
若是您猜对了,被打印出来的就是 undefined 。这种“奇怪的结果”的缘由是做为事件监听的回调(在咱们的例子中是 this.onClick),它会把目标元素绑定在“this”上。这意味着,当 onClick 被调用时,它内部的“this”是按钮的 DOM 对象(elem),而不是咱们的 myButton 对象,由于按钮的 DOM 对象没有 buttonName 的属性,因此控制台输出 undefined。
可是有办法解决这个问题(双关语)。咱们须要作的就是添加一行代码,仅需一行代码。
var myButton = {
elem: document.getElementById(‘button’),
buttonName: ‘My Precious Button’,
init: function() {
this.onClick = this.onClick.bind(this);
this.elem.addEventListener(‘click’, this.onClick);
},
onClick: function() {
console.log(this.buttonName);
}
};
复制代码
注意上面的代码片断(#21)中调用函数 init 的方式。确切地说,隐式绑定将 myButton 绑定到 init 函数的“this”上。如今注意,咱们新加的代码行是如何把 myButton 绑定到 onClick 函数。这样作会建立一个新的函数,除了它内部的“this”指向了 myButton,其余就和 onClick 彻底同样。而后新建立的函数被从新分配给 myButton.onClick。这就是所有操做,当你点击按钮时,你将看到控制台上输出“My Precious Button”。
你也能够经过箭头函数来修复代码。就是这样。我将把这个问题留给你,让你思考一下这为何能够。
var myButton = {
elem: document.getElementById(‘button’),
buttonName: ‘My Precious Button’,
init: function() {
this.elem.addEventListener(‘click’, () => {
this.onClick.call(this);
});
},
onClick: function() {
console.log(this.buttonName);
}
};
复制代码
var myButton = {
elem: document.getElementById(‘button’),
buttonName: ‘My Precious Button’,
init: function() {
this.elem.addEventListener(‘click’, () => {
console.log(this.buttonName);
});
}
};
复制代码
好了。咱们差很少就要结束了。还有一些问题,好比绑定是否有优先顺序?若是两个规则都试图将“this”绑定到同一个函数,这样的冲突该怎么办?这是另外一篇文章的主题了。第3部分?可能吧,可是老实说,你不多会遇到这样的冲突。因此如今咱们已经所有讲完了,让咱们总结一下咱们在这两部分学到的东西。
在第一部分中,咱们看到函数的“this”是如何变化的,而且如何根据函数的调用方式而改变。咱们讨论了默认绑定规则,它适用于函数的独立调用,而隐式绑定规则适用于调用函数时,前面有一个对象引用和箭头函数,以及它们如何使用词法绑定。在第一部分的结尾处,咱们还快速的介绍了在 JavaScript 对象中进行自调用。
在第二部分,咱们从 new 绑定开始,并讨论它是如何工做以及如何可以轻松地破坏整个结构。这一部分的后半部分致力于使用 call ,apply 和 bind 显式地将'this'绑定到函数。我还略显尴尬地与你分享了关于如何记住 call 和 apply 之间差别的助记符。但愿你能记住它。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。