[JS] You Don't Know JavaScript [this & Object Prototypes] - this All Makes Sense Now! [下]

前言

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(); // 2
obj2.foo(); // 3

obj1.foo.call( obj2 ); // 3
obj2.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 ); // 2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.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 ); // 2

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.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; // p1p2
bax.val; // p1p3

Determining this

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

  1. 如果是函数使用new binding那麽this所指向的便是被创建出来的新物件
var bar = new foo(); // this指向bar 
  1. 函数通过显性绑定(call或apply)或是bind的硬性绑定,则this指向被绑定的物件。
var bar = foo.call(obj2); //this指向obj2
  1. 函数通过环境物件而被调用(隐性绑定),则this指向呼叫function reference的环境物件。
const = obj1 = {
    foo: foo
}
obj1.foo();
  1. 不符合以上条件则属於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来来达到。

// ES5
function foo(a,b) {
	console.log( "a:" + a + ", b:" + b );
}

// spreading out array as parameters
foo.apply( null, [2, 3] ); // a:2, b:3

// ES6
foo(...[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 global
var 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 function
	return (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 global

foo.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 global

foo.call( obj ); // 2

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


结论

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

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

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


参考文献:You Don't Know JavaScript


<<:  资安即国安,台湾需要更多的CISSP!

>>:  老师!我想知道!要怎麽让终端机变漂亮呢 - Mac 篇

[2021铁人赛 Day11] General Skills 08

引言 昨天学到 ssh 以及 「大括号的分配律」─ Brace Expansion 这边再补充一点...

Eureka 介绍

Eureka 介绍 ...

[Day 14] 用 MLFlow 记录模型实验,就。很。快

前言 在整理实验结果之前,先来说说怎麽纪录实验~~ 你484常常听到以下对话 A: 哭啊,明天Mee...

新新新手阅读 Angular 文件 - ngFor(1) - Day19

本文内容 阅读有关 Angular 中有 ngFor 语法的笔记内容。 ngFor 在干嘛的? 它用...