对于 JavaScript
,其实一直没有系统性的学习过。都是有需要再去查相关语法和 API,自然在使用过程中也存在很多困惑。这次就来探讨下 JavaScript
中的 this
。
this 的动态指向性
在大多数常见的面向对象的语言中,this
通常指向类的实例,如 Java
的 this
, Python
的 self
,PHP
的 $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
中有三个方法 call
,apply
,bind
都是用于显式绑定的。
显式绑定是将 this
显式地绑定到一个上下文中。 这样能避免代码中的错误绑定和丢失绑定问题,同时也更便于执行重构和实现一些设计模式。
call
和 apply
的作用都一样,使用指定的上下文对象运行方法,不同的只是传参方式。
考虑下面的代码:
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
的上下文执行 test2
的 print
,此时在 print
里取到的 this
就是 test1
。
假设调用的方法有参数,通过下面的方式传递参,这也是 call
和 apply
唯一的区别:
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
总结
- 默认绑定会指向全局对象,带来不可控的影响。避免默认绑定或使用严格模式
- 隐式绑定和
new
绑定都可以确保this
能得到应有的指向 - 在某些情况下,也需要显式绑定来修改和确保
this
的指向为我们所想