[JS] You Don't Know JavaScript [this & Object Proto

前言

在this All Makes Sense Now! [上]中我们介绍了什么是call-site与4种绑定的规则,我们需要做的就是观察一个程式找到他的call-site并了解他适用于哪个规则,这样就可以找到this所指向的是什么了。

Everything In Order

透过找到call-site与适用的规则可以找到this所指向的地方,但是如果一个call-site符合多个规则那怎么办?这些规则具有优先顺序,接下来我们将介绍这些规则的顺序,首先要先了解default binding是所有规则中优先级最低的,所以先不讨论。

我们先讨论一下是implicit binding还是explicit binding?

function foo() {console.log( this.a );}var obj1 = {a: 2,foo: foo};var obj2 = {a: 3,foo: foo};obj1.foo(); // 2obj2.foo(); // 3obj1.foo.call( obj2 ); // 3obj2.foo.call( obj1 ); // 2

上面的程式码中可以看到当对function使用显性绑定后,显性绑定的规则会高于隐性绑定,这意味着当你需要使用隐性绑定的时候需要检查funciton是否有被显性绑定住。

接下来要看看new binding的优先级是如何。

function foo(something) {this.a = something;}var obj1 = {foo: foo};var obj2 = {};obj1.foo( 2 );console.log( obj1.a ); // 2obj1.foo.call( obj2, 3 );console.log( obj2.a ); // 3var bar = new obj1.foo( 4 );console.log( obj1.a ); // 2console.log( bar.a ); // 4

new binding的优先级是比隐性绑定高的,但是要如何确定new binding与显性绑定谁的优先极高呢?

function foo(something) {this.a = something;}var obj1 = {};var bar = foo.bind( obj1 );bar( 2 );console.log( obj1.a ); // 2var baz = new bar( 3 );console.log( obj1.a ); // 2console.log( baz.a ); // 3

bar强制绑定着obj1,之后使用new binding将bar中函数的this指向这个物件,但是结果却是用new创出来的baz没有改变到obj1.a的值,因为使用new binding后回传的会是一个新的物件并将函数的this指向这个新物件,所以他与bar中绑定的obj1是没关係的。

Currying

Currying(柯里化),又称为 parital application 或 partial evaluation,是个「将一个接受 n 个参数的 function,转变成 n 个只接受一个参数的 function」的过程。

bind(...)可以将输入的参数(需绑定物件后面的参数)默认的当作前函数的标準参数

function foo(p1,p2) {this.val = p1 + p2;}var bar = foo.bind( null, "p1" ); // 将"p1"默认的当作每次呼叫foo的参数var baz = new bar( "p2" );var bax = new bar( "p3" );baz.val; // p1p2bax.val; // p1p3

Determining this

了解了绑定的优先级,我们可以总结一下判断this的规则。

如果是函数使用new binding那么this所指向的便是被创建出来的新物件
var bar = new foo(); // this指向bar 
函数通过显性绑定(call或apply)或是bind的硬性绑定,则this指向被绑定的物件。
var bar = foo.call(obj2); //this指向obj2
函数通过环境物件而被调用(隐性绑定),则this指向呼叫function reference的环境物件。
const = obj1 = {    foo: foo}obj1.foo();
不符合以上条件则属于default binding,this在非严格模式下指向全域物件,否则是undefined。

Binding Exceptions

凡事均有特例,对于this的判定也不例外。

Ignored this

如果传递nullundefined给call,apply或bind的参数,那么这些值就会被忽略绑定规则会自动回到default binding。

function foo() {console.log( this.a );}var a = 2;foo.call( null ); // 2

既然传递非法的参数(null,undefined)会造成binding的忽略那么为什么会需要使用?

在ES6中我们可以使用扩展运算符(...)来将阵列中的元素拆开,但是在ES6之前要达到同样的效果需要使用apply来来达到。

// ES5function foo(a,b) {console.log( "a:" + a + ", b:" + b );}// spreading out array as parametersfoo.apply( null, [2, 3] ); // a:2, b:3// ES6foo(...[2, 3]); // a:2, b:3

将null当作参数传递给bind(...)可以达到currying的功能。

function foo(a,b) {console.log( "a:" + a + ", b:" + b );}var bar = foo.bind( null, 2 );bar( 3 ); // a:2, b:3

Indirection

若是创建了函数的间接引用(indirect reference),这种情况下会失去原本的bind而导致变回适用default binding。

function foo() {console.log( this.a );}var a = 2; // declaration in globalvar o = { a: 3, foo: foo };var p = { a: 4 };o.foo(); // 3(p.foo = o.foo)(); // 2

对于p.foo = o.foo来说,虽然看起来像是将o物件中的foo(...)赋予给p物件,但实际上p所拿到的是foo(...)本体的reference,代表着他的位置与o物件中的foo是不一样的,所以只适用default binding。


Lexical this

对于普通函数来说我们可以遵守4条绑定规则中找到this所指向的位置,但是ES6引入了一个不适用于这些规则的函数箭头函数

箭头函数不採用4个标準的绑定规则而是从封闭的(函数或全域)範围採用this绑定。

function foo() {// return an arrow functionreturn (a) => {// `this` here is lexically adopted from `foo()`console.log( this.a );};}var obj1 = {a: 2};var obj2 = {a: 3};var bar = foo.call( obj1 );bar.call( obj2 ); // 2, not 3!

在foo(...)中创建一个箭头函数,这个箭头函数会自动绑定foo()被调用时的this,以上面的例子来说当foo(...)被呼叫并且this被绑定为obj1,那么箭头函数中的this也会绑定obj1,而且箭头函数的绑定是不能被覆盖的

最常见的用法式将箭头函数应用在callback function中

function foo() {setTimeout(() => {// `this` here is lexically adopted from `foo()`console.log( this.a );},100);}var obj = {a: 2};var a = 4; // declaration in globalfoo.call( obj ); // 2

使用箭头函数当作callback function的参数传递给setTimeout并不会像我们在this All Makes Sense Now! [上]提到的,因为隐性赋值而产生binding遗失,箭头函数的this会在foo()被呼叫的时候就指定给foo()所绑定的物件。

其实这种绑定的方式在ES6的箭头函数出来之前就有类似的方式了

function foo() {var self = this; // lexical capture of `this`setTimeout( function(){console.log( self.a );}, 100 );}var obj = {a: 2};var a = 4; // declaration in globalfoo.call( obj ); // 2

透过在一进function后就先捕获foo中this所指向的物件,这样就不会因为隐性赋值的问题导致binding遗失。


结论

当我们需要找到函数中this所指向的位置,我们需要找到这个函数的call-site与看这个函数符合4种绑定规则的哪一种。

通过new调用funciton则this绑定新创建的物件。通过callapplybind调用则this绑定指定的物件。通过环境物件调用函数reference则this绑定呼叫函数的环境物件。default,严格模式下是undefined否则this指向全域物件

ES6提供的箭头函数不遵守上面的4个绑定规则,箭头函数的this绑定取决于他被创建的当下所绑定的物件。


参考文献:You Don't Know JavaScript


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章