对于 JavaScript,其实一直没有系统性的学习过。都是有需要再去查相关语法和 API,自然在使用过程中也存在很多困惑。这次就来探讨下 JavaScript 中的 this

this 的动态指向性

在大多数常见的面向对象的语言中,this 通常指向类的实例,如 Javathis, PythonselfPHP$this

于是乎,大多数人在接触 JavaScript 时也下意识认为 this 是指向类的实例。实际上在 JavaScript 里根本就没有真正意义上的类,也不区分类和实例的概念,是通过 prototype 来实现的面向对象(在 ES6 里已经引入了 class 关键字,但本质上还只是 prototype 的语法糖)。

默认绑定

在浏览器的 Developer Tools 中的 Console 输入 this 并回车,会输出以下信息:

Window {parent: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}

this 没有在任何其它对象中时,默认指向全局对象(全局作用域)。在浏览器中这个全局对象就是 window。(在 Node.js 环境中没有全局对象,所以会是 undefined

这种默认指向全局对象的方式就是 this 的默认绑定。

但默认绑定常常会带来更多的问题,任何代码都可能污染全局对象。不应该使用全局变量来保存数据。所以 ES5 引入了严格模式

严格模式使用 "use strict"; 声明,会取消默认绑定:

"use strict";

console.log(this);  // undefined

隐式绑定

隐式绑定很简单,当函数作为一个对象的属性时,该函数的 this 指向该对象。

var hello = {
  str: 'world',
  print: function() {
    console.log('hello', this.str);
  }
}
hello.print();  // hello world

默认绑定或许也可以看成全局函数默认作为全局对象的一个属性,从而使得全局函数的 this 指向全局对象。

显式绑定

React 中常常会看到类似的代码:

class Test extends React.Component {
  constructor(props) {
    super(props);
    this.testBtnClick = this.testBtnClick.bind(this);
  }
  
  testBtnClick() {
    console.log('click');
  }
  
  render() {
    return (
      <div> 
        <button onClick={this.testBtnClick}>test button</button>
      </div>
    );
  }
}

ReactDOM.render(
  <Test/>,
  document.getElementById('example')
);

其中的第四行 this.testBtnClick = this.testBtnClick.bind(this); 显式地绑定了 testBtnClick 这个函数的 this

Function.prototype 中有三个方法 callapplybind 都是用于显式绑定的。

显式绑定是将 this 显式地绑定到一个上下文中。 这样能避免代码中的错误绑定和丢失绑定问题,同时也更便于执行重构和实现一些设计模式。

callapply 的作用都一样,使用指定的上下文对象运行方法,不同的只是传参方式。

考虑下面的代码:

var test1 = {
  name: '',
  init: function() {
    this.name = 'aaa';
  },
  print: function() {
    console.log("hello1", this.name);
  }
}

var test2 = {
  name: '',
  init: function() {
    this.name = 'bbb';
  },
  print: function() {
    console.log("hello2", this.name);
  }
}

test1.init();
test1.print();  // hello1 aaa

test2.init();
test2.print();  // hello2 bbb

test2.print.call(test1);    // hello2 aaa

在最后一行,通过 call 使用 test1 的上下文执行 test2print,此时在 print 里取到的 this 就是 test1

假设调用的方法有参数,通过下面的方式传递参,这也是 callapply 唯一的区别:

test2.print.call(test1, param1, param2);
test2.print.apply(test1, [param1, param2]);

bind 的作用是返回一个绑定指定上下文对象的新方法

修改改改的例子:

var test1 = {
  name: '',
  init: function() {
    this.name = 'aaa';
  },
  print: function() {
    console.log("hello1", this.name);
  }
}

var test2 = {
  name: '',
  init: function() {
    this.name = 'bbb';
  },
  print: function() {
    console.log("hello2", this.name);
  }
}

test1.init();
test2.init();

var newPrint = test2.print.bind(test1);
newPrint(); // hello2 aaa

// 或者直接赋值回去
test2.print = test2.print.bind(test1);
test2.print();  // hello2 aaa

在实际中会有一个常见的问题,在控件的事件回调方法里传入我们的函数,函数中通过 this 调用当前对象的某些属性或方法。

而当回调执行时,是在浏览器中运行的,上下文被改变。此时函数里的 this 丢失绑定指向 window,就会出现错误。这也正是 React 常在 constructor 中使用 bind 显式绑定函数的 this 的原因。

new 绑定

在 ES6 前,通常用这种原型链方式创建一个对象:

function Hello(name) {
  this.name = name || 'world';
}
Hello.prototype.print = function() {
  console.log('hello', this.name);
};

var h = new Hello();
h.print();  // hello world

构造函数默认返回 this,并且该 this 绑定指向新创建的对象。

所以通过 new 创建的对象,this 绑定在这个新创建的对象上。

箭头函数

在各种事件和 HTTP 请求的回调中可能会用到 this,但就会发生前面讲的丢失绑定问题。

但箭头函数就可以解决该问题,箭头函数的 this 总是指向词法作用域,即其宿主。

例如以下代码:

var Hello1 = {
  str: 'world',
  print: function() {
    var p = function() {
      console.log('hello', this.str);
    }
    return p();
  }
}

var Hello2 = {
  str: 'world',
  print: function() {
    var p = () => {
      console.log('hello', this.str);
    }
    return p();
  }
}

Hello1.print(); // hello undefined
Hello2.print(); // hello world

总结

  1. 默认绑定会指向全局对象,带来不可控的影响。避免默认绑定或使用严格模式
  2. 隐式绑定和 new 绑定都可以确保 this 能得到应有的指向
  3. 在某些情况下,也需要显式绑定来修改和确保 this 的指向为我们所想