1

this

在 Java 等面向对象的语言中,this 关键字的含义是明确且具体的,即指代当前对象。一般在编译期确定下来,或称为编译期绑定。而在 JavaScript 中,this 是动态绑定,或称为运行期绑定的,这就导致 JavaScript 中的 this 关键字有能力具备多重含义,变得有点随意。而在ES6中又引入了Arrow Function以及Class,它们对于this指针也带来了一定的影响。

Global Context(全局上下文)

在任何函数之外的全局环境中,不管在不在strict模式中,this指针往往指向一个全局变量。

console.log(this.document === document); // true

// In web browsers, the window object is also the global object:
console.log(this === window); // true

this.a = 37;
console.log(window.a); // 37

Function Context(函数上下文)

简单调用

在某个函数中,this的值取决于该函数的调用者。无论是用hello("world”)还是call这种方式,都取决于传入该函数的对象。不过,在ES5的严格或者不严格模式下,同样的调用方式会有不同的结果。

function hello(thing) {  
  console.log("Hello " + thing);
}

// this:
hello("world")

// desugars to:
hello.call(window, "world");  

而如果是strict模式下:

// this:
hello("world")

// desugars to:
hello.call(undefined, "world");  

对象方法

另一种常用的调用函数的方法就是在object中调用:

function hello(thing) {  
  console.log(this + " says hello " + thing);
}

person = { name: "Brendan Eich" }  
person.hello = hello;

person.hello("world") // still desugars to person.hello.call(person, "world") [object Object] says hello world

hello("world") // "[object DOMWindow]world"  

bind

很多时候,需要为某个函数指定一个固定的this对象,最简单的方式即是使用闭包来获取一个不变的this对象。

var person = {  
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this.name + " says hello " + thing);
  }
}

var boundHello = function(thing) { return person.hello.call(person, thing); }

boundHello("world");  

不过,这种方式仍然存在着一定的问题,ES5为Function对象引入了一个新的bind方法来解决这个问题:

var boundHello = person.hello.bind(person);  
boundHello("world") // "Brendan Eich says hello world"  

这种方式在设置回调函数中的this指针的时候会起到很大的作用,特别是在React中,为了保证指针的稳定性,往往需要为内置方法设置bind。

var person = {  
  name: "Alex Russell",
  hello: function() { console.log(this.name + " says hello world"); }
}

$("#some-div").click(person.hello.bind(person));

// when the div is clicked, "Alex Russell says hello world" is printed

apply/call

在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。让我们看一个具体的例子:

function Point(x, y){ 
    this.x = x; 
    this.y = y; 
    this.moveTo = function(x, y){ 
        this.x = x; 
        this.y = y; 
    } 
 } 

 var p1 = new Point(0, 0); 
 var p2 = {x: 0, y: 0}; 
 p1.moveTo(1, 1); 
 p1.moveTo.apply(p2, [10, 10]);

array.forEach

在这样一个回调函数中,回调函数的this指针是由调用者决定的,完整的forEach声明如下:array.forEach(callback[, thisArg]),这个传入的thisArg即是回调的调用者。

var o={
   v:'hello',
   p:['a1','a2'],
   f:function f(){
      this.p.forEach(function (item){
           console.log(this.v+' '+item);
      });
   }
}

o.f();
//undefined a1
//undefined a2

Arrow Function

Arrow Function是ES6新增的特性,很类似于Java或者C#中的Lambda表达式。Arrow函数中的this指针在创建时就被绑定到了闭合的作用域内,不会收到new、bind、call以及apply这些方法的影响。

var o = {
  traditionalFunc: function () {
    // Normal function, bound as expected
    console.log('traditionalFunc this === o?', this === o);
  },
  arrowFunc: () => {
    // Arrow function, bound to scope where it's created
    console.log('arrowFunc this === o?', this === o);
    console.log('arrowFunc this === window?', this === window);
  }
};

o.traditionalFunc();
// traditionalFunc this === o? true

o.arrowFunc();
// arrowFunc this === o? false
// arrowFunc this === window? true

上述代码中的arrowFunc隶属于o对象,但是在window的作用域中被创建,因此,最终arrow函数中的this指针的值等于window对象。ES5中的对于this的控制已然非常复杂,特别是在处理异步代码中如何传入合适的this对象也是一件麻烦事,如下文所示:

var asyncFunction = (param, callback) => {
  window.setTimeout(() => {
    callback(param);
  }, 1);
};

// With a traditional function if we don't control 
// the context then can we lose control of `this`.
var o = {
  doSomething: function () {
    // Here we pass `o` into the async function, 
    // expecting it back as `param`
    asyncFunction(o, function (param) {
      // We made a mistake of thinking `this` is 
      // the instance of `o`.
      console.log('param === this?', param === this);
    });
  }
};

o.doSomething(); // param === this? false

为了绑定上述代码中的this指针,一般来说有三个办法:

  • 为this指针创建一个固定的引用。

var asyncFunction = (param, callback) => {
  window.setTimeout(() => {
    callback(param);
  }, 1);
};

// Define a reference to `this` outside of the callback, 
// but within the callback's lexical scope
var o = {
  doSomething: function () {
    var self = this;
    // Here we pass `o` into the async function, 
    // expecting it back as `param`
    asyncFunction(o, function (param) {
      console.log('param === this?', param === self);
    });
  }
};

o.doSomething(); // param === this? true
  • 使用bind方法绑定this

var asyncFunction = (param, callback) => {
  window.setTimeout(() => {
    callback(param);
  }, 1);
};

// Here we control the context of the callback using
// `bind` ensuring `this` is correct
var o = {
  doSomething: function () {
    // Here we pass `o` into the async function, 
    // expecting it back as `param`
    asyncFunction(o, function (param) {
      console.log('param === this?', param === this);
    }.bind(this));
  }
};

o.doSomething(); // param === this? true
  • 使用Arrow Function在创建时绑定this指针。

var asyncFunction = (param, callback) => {
  window.setTimeout(() => {
    callback(param);
  }, 1);
};

var o = {
  doSomething: function () {
    // Here we pass `o` into the async function, 
    // expecting it back as `param`. 
    //
    // Because this arrow function is created within  
    // the scope of `doSomething` it is bound to this 
    // lexical scope.
    asyncFunction(o, (param) => {
      console.log('param === this?', param === this);
    });
  }
};

o.doSomething(); // param === this? true

DOM Event handler(Dom事件)

当某个函数作为事件监听器时,它的this值往往被设置为它的调用者。

// When called as a listener, turns the related element blue
function bluify(e){
  // Always true
  console.log(this === e.currentTarget); 
  // true when currentTarget and target are the same object
  console.log(this === e.target);
  this.style.backgroundColor = '#A5D9F3';
}

// Get a list of every element in the document
var elements = document.getElementsByTagName('*');

// Add bluify as a click listener so when the
// element is clicked on, it turns blue
for(var i=0 ; i<elements.length ; i++){
  elements[i].addEventListener('click', bluify, false);
}

如果是行内的事件监听者,this指针会被设置为其所在的DOM元素:

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

王下邀月熊_Chevalier
22.5k 声望8.5k 粉丝

爱代码 爱生活 希望成为全栈整合师