前言
在this Or That?中提到了许多对于this
的误解,并且也对于这些误解做了一些解释,我们了解到this
是对每个函式调用的绑定,是基于被调用的位置而不是宣告的位置。
Call-site
为了了解this
,我们必须要先了解一个重要的观念call-site
,它代表着函式在程式中被调用的位置
,通常要找到call-site是要定位从何处调用此函式
,但通常这个行为并不是这么容易,因为某些模式下会掩盖掉真正的call-site,这个状态下我们需要考虑的是call-stack
,call-stack代表着呼叫function的堆叠,比如说
function c(){ console.log('c');}function b(){ c(); console.log('b');}function a(){ b(); console.log('a');}a();
上面的程式中呼叫a(...),而a(...)中又呼叫b(...),最后b(...)中又呼叫c(...),而call-stack -> a() -> b() -> c()。
而call-site来说,他是在他call-stack父层中呼叫自己的位置
,看其来很绕舌不过我们一样拿上面的程式码来做举例,对于c(...)
而言,他的call-site
就是在call-stack父层(b(...))
所呼叫的位置,以此类推。
function c(){ console.log('c');}function b(){ c(); // function c(...) => call-site console.log('b');}function a(){ b(); // function b(...) => call-site console.log('a');}a();
Nothing But Rules
介绍完call-site与call-stack,接下来我们将重点移到call-site是如何确定函数执行期间this
的指向,对于这个指向我们有4条规则
,我们先一一介绍是哪些规则。
Default Binding
第一种规则是来自函数最常见的情况函数独立调用(换句话说就是只呼叫自己而没有在内部嵌入其他函数)
,若没有其他规则适用,可以将这个规则是为万用规则。
function foo() {console.log( this.a );}var a = 2;foo(); // 2
以上面的程式中,default binding
代表着this
是绑定全域物件
,由于我们的var a = 2
是宣告在全域,所以这里的this才会指向到全域的a,我们要如何知道default binding规则适用于这个例子?
我们通过call-site来观察foo(...)是在哪里被调用的,在我们的程式中foo(...)是一个直白且无修饰的
函数呼叫,意味着他没有在内部嵌入其他函数,所以default binding规则在这里适用。
但是default binding对于使用严格模式来说就不适用
function foo ( ) { "use strict" ;console . log ( this . a ) ; }var a = 2 ;foo ( ) ; // TypeError: `this` is `undefined`
但是有一个特别的点,对于严格模式来说只要foo(...)的作用域内
不是严格模式,那么this
一样也可以绑定到全域物件。
function foo ( ) { console . log ( this . a ) ; }var a = 2 ;( function ( ) { "use strict" ;foo ( ) ; // 2 } ) ( ) ;
Implicit Binding
第二个规则是需要考虑call-site是否有一个环境物件(context object)。
function foo() {console.log( this.a );}var obj = {a: 2,foo: foo};obj.foo(); // 2
我们宣告了一个function foo(...)之后将他加入到obj物件中成为他的property,无论foo()是否一开始就在obj
上被宣告或是后来才加入到obj
中(上面的例子),这个函数
都不被obj所真正的拥有或包含
,但是由于对于call-site来说obj环境来Reference
foo(...),所以可以说obj在函数被调用的时间点
拥有或包含这个funciton reference
。
当一个context object中有一个function reference则implicit binding规则会将这个fucntion中的this绑定这个object,所以以上面的例子来说foo(...)中的this所指向的就是obj
。
对于嵌套的物件来说,只有最后一层/最上层
物件才会对call-site起作用
function foo() {console.log( this.a );}var obj2 = {a: 42,foo: foo // call-site};var obj1 = {a: 2,obj2: obj2};obj1.obj2.foo(); // 42
Implicitly Lost
当一个implicitly bound的函数丢失了绑定,则会退回default binding
,至于指向的是全域物件还是undefined则取决于是否使用严格模式。
function foo() {console.log( this.a );}var obj = {a: 2,foo: foo};var bar = obj.foo; // loses that binding!var a = "oops, global"; // `a` is property on global objectbar(); // "oops, global"
虽然bar
似乎是obj.foo的reference,但是实际上他只是对foo(...)本体的另一个reference,换句话说虽然bar与obj.foo都是对foo(...)本体的reference,但是实际上是两个不一样的地方
,而且对于call-site而言呼叫bar(...)是一个直白且无修饰的
函数呼叫,所以他适用于default binding
。
还有一个更加微妙更常见更出乎意料的方式,当我们传递一个callback function时
function foo() {console.log( this.a );}function doFoo(fn) {// `fn` is just another reference to `foo`fn(); // <-- call-site!}var obj = {a: 2,foo: foo};var a = "oops, global"; // `a` also property on global objectdoFoo( obj.foo ); // "oops, global"
对于参数的传递来说他是一个隐性赋值
,而且如果要传递的参数是函数的话则是一个隐性的reference赋值
,所以结果会与上一个程式码相同。
function doFoo(var fn = obj.foo){ // 隐性function reference 赋值代表fn与obj.foo的reference是不同的。}
对于传递callback function作为参数会丢失binding这件事,除了自己定义的function之外对于原生的funciton也是一样的情况。
function foo() {console.log( this.a );}var obj = {a: 2,foo: foo};var a = "oops, global"; // `a` also property on global objectsetTimeout( obj.foo, 100 ); // "oops, global"
可以将他看为
function setTimeout(var fn = obj.foo, delay){ //隐性function reference 赋值}
Explicit Binding
我们介绍了Implicit Binding如果需要间接地将函数中的this绑定到这个物件上,会需要对这个物件做一些改变(将function reference引入到物件属性中),但是有没有方法是可以不更改物件的型态却又可以使function的this绑定着这个物件的呢?
我们可以使用JS所提供function的prototype(后面会介绍)call(...)
或apply(...)
method,他们的第一个参数都是一个物件,他代表着我这个fucntion的this所指向的目标,因为明确的指出this要指向什么所以我们称这种方式为Explicit Binding。
function foo() {console.log( this.a );}var obj = {a: 2};var a = 5; // declaration in global foo.call( obj ); // 2
通过foo.call(...)的方式将this明确的指向obj,注意的是如果对于call(...)或apply(...)的第一个参数传递的不是一个物件(string,boolean,number...)那么传递的这个参数的类性会被包装在物件(new String(...), new Boolean(...), new Number(...))这种行为称为boxing
。
Hard Binding
虽然可以对单独的function进行显性绑定,但是依然无法解决上面提到的赋值导致绑定丢失的问题,但是可以有一种明确绑定的变种可以解决这个问题。
function foo() {console.log( this.a );}var obj = {a: 2};var a = 4; // declaration in globalvar bar = function() {foo.call( obj );};bar(); // 2setTimeout( bar, 100 ); // 2// `bar` hard binds `foo`'s `this` to `obj`// so that it cannot be overridenbar.call( window ); // 2
我们在bar内部强制绑定了foo(...)的this指向obj,所以无论之后怎么调用bar(...)在他的内部都会自动的强制绑定obj,这种行为我们称为hard binding
。
对于hard binding来说,ES5中提供了funciton.prototype.bind
可以将物件强制绑定给函数。
function foo(something) {console.log( this.a, something );return this.a + something;}var obj = {a: 2};var bar = foo.bind( obj );var b = bar( 3 ); // 2 3console.log( b ); // 5
API Call "Contexts
在许多现在JS的内建函数中都有提供一个可选的参数通常称为context,这种设计可以让你直接填入你需要绑定的object而不必一定要使用bind(...)
。
function foo(el) {console.log( el, this.id );}var obj = {id: "awesome"};// use `obj` as `this` for `foo(..)` calls[1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome
arr.forEach(function callback(currentValue[, index[, array]]) { //your iterator}[, thisArg]);/* callback : 把 Array 中的每一个元素作为参数,带进本 callback function中 currentValue : 当前被处理的Array元素 index(可选):当前被处理的Array元素的index array(可选):forEach()本身的Array -> arr thisArg(context)(可选):callback function的this (需要绑定的物件)*/
New Binding
在传统拥有class的语言中,constructor是一个特殊的method,当一个class被new
实体化后这个constructor就会被调用以用来初始化这个class。
something = new MyClass(...);
虽然JavaScript中也有new
但是他与其他语言的new是没有关係的,对于JavaScript来说constructor就只是个函数
他们偶然的与new一起被调用,但他却不依附于也不会初始化一个class。
当一个函数前面加上new调用,也就是constructor调用时,会自动完成以下的事情:
凭空创造一个全新的物件。被创建的物件会接入原形鍊([[prototype]]-Link)。被调用funciton中的this被设定为指向新的物件。除非function return属于自身的物件,否则这个new调用的function会自动return
这个新创建的物件。function foo(a) {this.a = a;}var bar = new foo( 2 );console.log( bar.a ); // 2
使用new来调用foo(...)等于我们建立了一个新的物件并将function中的this指向这个新创出来的物件,这种绑定新建出物件的方法称为new binding。
参考文献:You Don't Know JavaScript