在函数作用域中使用this,想一想有没有一些场景让你感到困惑,this指向不是你期望的结果。一时又不知道问题出在哪,需要重新去看this相关的知识,这篇文章就要详细解释this。
先看这个代码,例子1:
var str = 'window';
var obj = {
str: 'foo',
getStr: function getStr(){
return this.str;
}
}
console.log(obj.getStr()); // "foo"
调用obj的getStr方法,和期望的一样,this指向obj。有时我们只是想每次都要通过obj调用getStr,写起来不方便,通过一个变量缓存obj.getStr方法达到简写的目的。
例子2:
var str = 'window';
var obj = {
str: 'foo',
getStr: function getStr(){
return this.str;
}
}
var fn = obj.getStr;
console.log(fn()); // "window"
但是调用之后发现this的指向变成了window。很多人的困惑就在这里,不明白为什么是window。
之前的系列文章内存机制中提到了引用类型保存在堆内存中。对象obj是引用类型变量,它存的是一个内存地址,要获取obj的值,先要读它的内存地址,通过这个内存地址去堆内存中找到对应的具体对象。
堆内存中对象的具体值可用对象字面量的键值对表示,键对应的具体值又可用一个对象的字面量表示,可以通过这行代码查看:
Object.getOwnPropertyDescriptor(obj, 'str');
控制台会打印出一个对象字面量,里面包含这些信息:
{
configurable: true, // 是否可配置化
enumerable: true, // 是否可枚举
value: "foo", // 具体值
writable: true, // 是否可写
__proto__: Object // __proto__指向构造函数的prototype属性
}
通过value属性获取str的具体值,如果堆内存中的键对应的是一个函数(getStr),打印看看:
Object.getOwnPropertyDescriptor(obj, 'getStr');
结果:
{
configurable: true,
enumerable: true,
value: f(), // 值是函数引用<fn reference>,对应堆内存中一个具体函数
writable: true,
__proto__: Object
}
value属性其实对应一个内存地址,这个地址又对应一个具体的函数。
说回主题,obj.getStr和fn指向同一个函数内存地址:
var fn = obj.getStr;
console.log(obj.getStr===fn); // true
虽然同一内存地址的函数被引用,可this的指向被确立时并不关心这个,它关心的是:this所在函数的调用者是谁?
对比前面例子1和例子2两段代码,函数通过obj.getStr()调用,this指向obj,在非严格模式下,独立调用fn函数,内部this默认指向window。
我们可以先得出这样一个结论this指向调用它的那个对象,网上很多文章都是这样说的,这样的解释也可以覆盖大部分的实际场景。可也有例外。
把上面的例子稍做修改:
var str = 'window';
var obj = {
str: 'foo',
getStr: function getStr(){
return this.str;
}
}
var fn = obj.getStr.bind(obj);
console.log(fn()); // "foo"
在缓存obj.getStr方法时,后面加了bind(obj),fn方法的调用者还是window,然而this却指向obj。这么看之前那个结论this指向调用它的那个对象就不准确了。
还是从函数执行上下文的角度分析一下:
当一个执行上下文入栈时,它有两个生命周期,创建和执行阶段。在创建阶段又干了三件事:生成变量对象(只有变量名字,未赋值)、建立作用域链、还有确定this指向。生成变量对象和建立作用域链前文已经介绍过。
this的指向也是在执行上下文的创建阶段确立的,重要的一点是this是函数在被调用的时候,函数执行上下文被创建阶段确立的,this的指向很灵活,可以通过一些方法修改。JS中可以通过call、apply、bind三个方法修改this指向,前面举的例子就用了bind方法。
call方法:
var str = 'window';
var obj = {
str: 'foo',
getStr: function getStr(){
return this.str;
}
}
console.log(obj.getStr.call()); // "window"
call方法不传参数情况下默认是全局对象。
apply方法:
var str = 'window';
var obj = {
str: 'foo',
getStr: function getStr(){
return this.str;
}
}
console.log(obj.getStr.apply()); // "window"
apply和call方法一样,只是传参的方式不同。bind方法前面已经举过例子。 所以在说出那个结论this指向调用它的那个对象时,要补充一句,通过call、apply、bind三个方法调用时要看指定给哪个对象。
顺便提一下React中出现的场景:
class MyClass extends React.Component{
constructor(props){
super(props);
...
}
...
handleClick(){
this.setState({
...
})
}
render(){
return (
<div>
...
<button onClick={this.handleClick.bind(this)}>click</button>
...
</div>
)
}
}
写过React的人一定知道class写法中给某个虚拟DOM绑定点击事件handleClick时要在handleClick函数后面加上bind(this)将this指向当前class,如果不这么做handleClick内部用到this.setState之类的方法时,this指向undefined,这又关系到严格模式。
严格模式: 前面举的例子都在非严格模式下进行,现在说说严格模式:
var obj = {
fn1: function(){
return this;
},
fn2: function(){
'use strict';
return this;
}
}
var fn1 = obj.fn1;
var fn2 = obj.fn2;
console.log(fn1()); // window (独立调用)
console.log(window.fn1()); // window (被拥有者window调用)
console.log(fn2()); // undefined (独立调用)
console.log(window.fn2()); // window (被拥有者window调用)
- 首先要区分一下独立调用和被拥有者调用,fn1() 和 fn2() 是独立调用,window.fn1() 和 window.fn2() 被拥有者window调用
- fn1内部使用非严格模式,fn2内部是严格模式。
- 独立调用函数时,非严格模式下的内部的this默认指向全局对象window,严格模式下this被禁止指向全局对象window。
面向对象开发中通过new操作改变this指向
注:这里涉及到面向对象的相关内容,后面的文章会具体说明。但是这里也会用到this,这里做简单描述。
看一段代码:
// 构造函数 Car
function Car(color) {
this.color = color;
}
// 原型方法 getColor
Car.prototype.getColor = function() {
return this.color;
}
// 通过 new 操作符实例化对象 myCar
var myCar = new Car('黑色');
console.log(myCar.getColor()); // '黑色'
先说明这是非严格模式。按照之前的总结,Car('黑色') 这么调用的话,内部this应该指向window,但是它前面有new操作符,new的作用是把后面跟的函数当作构造函数(一个模板),构造出的一个实例化对象myCar,构造函数Car里面用this定义过的属性和方法,还有在prototype属性上定义的属性和方法会自动复制到这个实例化的对象myCar。this也指向了实例化对象myCar。
最后举一个比较迷惑人的例子:
var str = 'window';
var obj = {
str: 'foo',
getStr: this.str + '-bar'
}
console.log(obj.getStr);
思考一下会输出什么? 答案是:
'window-bar'
this指向了全局作用域window,注意本文一直说的是函数执行上下文创建阶段确立的this指向,上例并没有出现函数和函数调用,obj对象的大括号在ES5中不能形成块级作用域,因此this指向window。
总结一下
要注意函数内部是否使用了严格模式,函数被调用时看看调用这个函数的对象(或者说是函数的拥有者)是谁,注意call、apply、bind这三个可以修改this指向的方法,面向对象开发中熟练掌握new实例化对象后的原理。