JavaScript学习(六) —— this

217 阅读6分钟

在函数作用域中使用this,想一想有没有一些场景让你感到困惑,this指向不是你期望的结果。一时又不知道问题出在哪,需要重新去看this相关的知识,这篇文章就要详细解释this。

image.png

先看这个代码,例子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的值,先要读它的内存地址,通过这个内存地址去堆内存中找到对应的具体对象。

image.png

堆内存中对象的具体值可用对象字面量的键值对表示,键对应的具体值又可用一个对象的字面量表示,可以通过这行代码查看:

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指向调用它的那个对象就不准确了。

还是从函数执行上下文的角度分析一下:

image.png

当一个执行上下文入栈时,它有两个生命周期,创建和执行阶段。在创建阶段又干了三件事:生成变量对象(只有变量名字,未赋值)、建立作用域链、还有确定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实例化对象后的原理。