5.彻底搞懂javascript-this

1,264 阅读7分钟

在第二章运行上下文(Execution Context)中我们提高运行上下文的结构:

function ExecutionContext() {
    this.LexicalEnvironment = undefined;
    this.VariableEnvironment =  undefined;
    this.ThisBinding = undefined;
}

这篇就来聊聊这个ThisBinding。当前运行上下文的ThisBinding其实就是在当前运行上下文上执行代码里this的值。所以你在代码执行的时候遇到"this",就会来找当前运行上下文的这个ThisBinding作为this的值。那么,ThisBinding的值是多少呢?

要知道ThisBinding的值是多少,ExecutionContext什么时候被创建,创建时ThisBinding被设置为什么。

而我们又知道JS代码在三种情况下会创建ExecutionContext:

可运行代码(Executable Code)

ECMAScript 5 规范,定义了三类可运行代码(Executable Code) ,运行这些代码时候会创建运行上下文(Execution Contexts):

  • global code:就是js整个“程序”,就是源代码文件中所有不在function体中的代码。
  • function code:就是函数体中的代码,除了内嵌函数体中的代码以外
  • eval code : 就是传给内置eval函数的代码字符串

只要我们了解运行这三种代码时候,创建了ExecutionContext的ThisBinding被设置为多少。就知道运行在该运行上下文的this的值了。

global code中的this

这个我们提过:

当JS引擎开始要进行global code代码运行之前,会先创建一个全局运行上下文(global execution context),并放入运行栈中:


//创建一个空的运行上下文
var globalExecutionContext = new ExecutionContext();

//创建全局词法环境
GlobalEnvironment = creatGlobalEnvironment(globalobject)//可以看作是浏览器环境下的window

//设置运行上下文
globalExecutionContext.LexicalEnvironment = GlobalEnvironment;
globalExecutionContext.VariableEnvironment = GlobalEnvironment;
globalExecutionContext.ThisBinding = globalobject;

Runtime.push(globalExecutionContext);

//这时的Runtime是这样的:
Runtime = {
   executionContextStack: [globalExecutionContext];
};

在进入程序代码之前,创建了全局运行上下文,其中的ThisBinding被设置为了全局对象:globalExecutionContext.ThisBinding = globalobject;

所以在全局代码中的this的值为为全局对象(浏览器下为window)。

函数中this

在function code里this的值就比较多变了,我们在函数调用一篇中提到,用不同的调用方式调用函数后,进入function code之前会设置不同thisArg,并传递到function code代码中 。进入function code后,会创建function的运行上下文,且设置其ThisBinding为thisArg。因此函数里的this的值,就和调用关系又很大的关系。

这里强调一下,函数中的this和函数的调用方式相关而与在哪被创建无关。与函数的词法环境的"静态"相比它是动态的。它之和当前运行上下文的ThisBings相关。

我们在函数运行讲过:

那这五种调用方式,在进入函数代码运行之前,携带进去的,要作为this的"东西"都是啥呢?

  1. 带undefined 进去的:函数调用functionName();和 立即调用函数表达式(function(){})(),(function functionName(){})();
  2. 带对象进去的:
    • 方法调用:如someObj.method() : 带someObj进去
    • new functionName() 方式的调用:创建一个新对象 newObject,带进去
    • functionName.call和functionName.apply:把call和apply指定thisArg带进去
  • 判断携带进来的thisArg的值:

    • 如果是strict,使barExecutionContext.ThisBinding = thisArg;
      • 不是strict
        • 如果thisArg是undefined,使barExecutionContext.ThisBinding = globalobject;
        • 如果thisArg不是undefined,使barExecutionContext.ThisBinding = toObject(thisArg);

因此函数中this的值就有几种情况:

普通函数调用

因为普通函数调用,包括调用functionName();和 立即调用函数表达式(function(){})(),(function functionName(){})();等,传到函数里的thisAarg是undefined。

因此,如果是非strict,则ThisBinding = globalobject;也就是:

  • 在非strict mode下,普通函数调用中this为全局对象
  • 在strict mode下,普通函数调用中this为undefined

方法调用

针对如someObj.method() : someObj作为 thisArg传进函数代码里,在创建函数运行上下文的时候,ThisBinding = someObj。

因此在方法调用模式someObj.method()中的this,为someObj。

var a = 2;
var someObj = {
    a:1
    print:function(){
        console.log(this.a)
    }
}

var outPrint = someObj.print;

someObj.print(); //1
outPrint();//2

outPrint()调用时,传进函数的thisArg是undefined,所在outPrint()的运行上下文中,ThisBinding被设置为全局对象,所以这时,this.a就是全局对象上的var a = 2;

而someObj.print()调用时,传进传进函数的thisArg是someObj,所以在someObj.print()运行上下文中,ThisBinding被设置someObj,所以这时this.a就是someObj上a:1。

注意,函数(箭头函数除外)里的this只和调用方式相关和在哪调用,函数在哪创建无关。

new 调用

  • new functionName() 方式的调用:创建一个新对象 newObject,带进去

在new方式调用的函数运行上下文中,ThisBinding = newObject,因此在new方式调用的函数中,this值就是新创建的对象newObject。

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p = Point(7, 5); // 没有new,普通调用,this为全局对象,在全局对象上创建x=7 y=5
var point = new Point(7, 5); //使用new调用,this为新创建的对象,在新对象上创建x=7,y=5

console.log(x); // 7
console.log(y); // 5

call,apply调用

call和apply调用,能够显示的传递给函数thisArg。其实不只是Function.prototype.call和Function.prototype.apply这个两个函数可以给调用的函数显示传递thisArg参数,下列的方法都给函数调用显示传递提供thisArg:

  • Function.prototype.apply( thisArg, argArray )
  • Function.prototype.call( thisArg [ , arg1 [ , arg2, ... ] ] )
  • Function.prototype.bind( thisArg [ , arg1 [ , arg2, ... ] ] )
  • Array.prototype.every( callbackfn [ , thisArg ] )
  • Array.prototype.some( callbackfn [ , thisArg ] )
  • Array.prototype.forEach( callbackfn [ , thisArg ] )
  • Array.prototype.map( callbackfn [ , thisArg ] )
  • Array.prototype.filter( callbackfn [ , thisArg ] )

需要注意的是如果传递给thisArg的null或者undefined,在非严格模式ixia函数中this还是全局对象,记得我们提到过,进入到函数代码,创建运行上下文之前的判断吗:

  • 判断携带进来的thisArg的值:

    • 如果是strict,使barExecutionContext.ThisBinding = thisArg;
      • 不是strict
        • 如果thisArg是undefined,使barExecutionContext.ThisBinding = globalobject;
        • 如果thisArg不是undefined,使barExecutionContext.ThisBinding = toObject(thisArg);
var obj = {
    a:1
};

function print() {
    console.log(this);
}


print.call(null);//window
print.call(undefined);//window
print.call(obj);//obj

bind

Function.prototype.bind,的只要功能是创建一个和原函数一样body和[[scope]]的函数,但是它的this则绑定为bind函数的第一个参数。在新函数中,无论函数如何被使用,this都会永久绑定到bind的第一个参数。

function f() {
  return this.a;
}

var g = f.bind({a: 'azerty'});
console.log(g()); // azerty

箭头函数

箭头函数是ES6的新内容,这里讲到this,顺便提一下。 箭头函数的this保存为它被创建时运行上下文的this的值。在global下创建箭头函数,为箭头函数this为global,保持为它被创建时的this值。在function里创建也一样,它的this保持为在箭头函数被创建时function运行上下文的this的值。

这个和我们前面提到的函数都不一样,前面提到的函数都是和调用时的运行上下文相关,箭头函数的this却和创建时的运行上下文相关。

还有一点就是,对箭头函数调用call apply 和 bind 来绑定this是无效,没有效果。

var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); // true


var obj = {func: foo};
console.log(obj.func() === globalObject); // true

console.log(foo.call(obj) === globalObject); // true

foo = foo.bind(obj);
console.log(foo() === globalObject); // true

事件绑定函数中this

分两种情况:

  • 作为event handler,addEventListener添加为元素的事件回调函数,其中this为触发该事件的元素对象(如果不是addEventListener添加,有些浏览器不遵循此规则)
  • inline event handler: this为监听函数所在的元素,只有最外层的this是这样,如果是是内层function里的this则为全局或者undefined
<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

function bluify(e) {
    console.log(this);
}


var elements = document.getElementsByTagName('button');
elements[0].addEventListener('click', bluify, false);

Eval Code 中的this

JS三种可运行代码,我们已经提到过global、function,只上下eval还没提到。因为eval比较少用,我们也不细究。这里大概讲下,进入eval代码以后,运行上下文的创建过程。

分两种情况:

  1. 直接调用,则运行上下文的创建过程相当于复制一份调用eval的那个运行上下文,在全局上直接调用eval,则复制一份全局运行上下文,在函数中直接运行eval,则复制函数的运行上下文,所以这个种情况下下,eval种的this和调用它的那个运行上下文的this相同。
eval("var definedInEval = 2;console.log(this);");//window 

console.log(definedInEval); //2 

var obj = {
    method: function () {
        eval('console.log(this)'); // obj
    }
}
obj.method(); 

  1. 非直接调用,不管在哪调用,则其运行上下文都是复制全局运行上下文,所以this都是全局对象
var evalcopy = eval;

evalcopy("console.log(this);");

var obj = {
    method: function () {
        evalcopy('console.log(this)'); //window
    }
}
obj.method();