阅读 738

JavaScript中Class的this指向

前言

JavaScript中的this指向问题本来是一个入门必会的问题,但是对于classthis的指向问题,发现不少人还有困惑。希望这篇文章能给大家讲清楚。

this的绑定优先级

关于this有不少说法。有的人说this是谁调用就指代谁。有的人说this跟作用域无关,只跟执行上下文有关。两种说法貌似是一个意思。

1.new创建出来的实例去调用方法,this指向当前实例

class Cat {
    jump() {
        console.log('jump',this)
    }
}
const cat = new Cat()
cat.jump() // jump Cat {}
复制代码

2.显式绑定

使用call、apply、bind

function jump() {
  console.log(this.name)
}
const obj = {
  name: '豆芽',
  jump,
}
jump = jump.bind(obj)
jump() // 豆芽
复制代码

3.对象中的方法绑定

function jump() {
  console.log(this.name)
}
const obj = {
  name: '豆芽',
  jump,
}
obj.jump() // 豆芽
复制代码

4.默认绑定

在严格模式下,thisundefined,否则是全局对象。

Class中属性与方法的绑定

class Cat {
  constructor(name) {
    this.name = name
  }
  jump() {
    console.log('jump', this)
  }
  static go() {
    console.log(this)
  }
}
Cat.drink = function() {
  console.log('drink', this)
}
Cat.prototype.eat = function() {
  console.log('eat', this)
}
Cat.prototype.walk = () => {
  console.log('walk', this)
}
let cat = new Cat('豆芽')
复制代码

image.png

通过上图可以看到,Cat所创建出来的实例,其方法挂载在实例的__proto__上面,即挂载在原型对象上。因为cat.proto 与 Cat.prototype指向同一个对象,所以当在cat.__proto__上挂载或者覆盖其原有方法时,所有由Cat所创建出来的实例,都将会共享该方法,所有实例都是通过__proto__属性产生的原型链到原型对象上寻找方法。

但是静态方法不会共享给实例,因为没有挂载在原型对象上面。

而属性是挂载在实例上的,即每一个创建出来的实例,都拥有自己不同值的属性。

Class中this的绑定

image.png

image.png

当我们打印typeof Cat可知Cat是函数类型,类本身就指向构造函数,ES6中的class类其实只是个语法糖,皆可以用ES5来实现。由构造函数Cat创建的实例cat是一个对象。在初始化cat实例的时候,在constructor中就会把this上的属性挂载到实例对象上面。

class Cat {
  constructor(name, age) {
    this.name = name
  }
  run() {
    console.log('run', this)
  }
}
let cat = new Cat('豆芽')
cat.name // '豆芽'
cat.run() // run Cat {name: '豆芽'}
复制代码

当调用cat.run()的时候,当前上下文是cat,所以其this指向的是cat这个实例。

class Cat {
  constructor(name) {
    this.name = name
    this.jump = this.jump.bind(this)
    this.drink = () => {
        console.log('drink',this)
    }
  }
  run() {
    console.log('run', this)
  }
  jump() {
    console.log('jump',this)
  }
  static go() { 
    console.log('go',this)
  } 
}
Cat.prototype.walk = () => {
    console.log('walk',this)
}

let cat = new Cat('豆芽')
let run = cat.run
let jump = cat.jump
let go = Cat.go
let walk = cat.walk
let drink = cat.drink

run() // run undefined (严格模式下,this为undefined,可以举一个严格模式下function的例子)
jump() // jump Cat {name: "豆芽", jump: ƒ}
Cat.go() // go class Cat {}
go() // go undefined
cat.walk() // walk Window
walk() // walk Window
cat.drink() // drink Cat {name: "豆芽", jump: ƒ, drink: ƒ}
drink() // drink Cat {name: "豆芽", jump: ƒ, drink: ƒ}
复制代码

解析:

run方法: 当把实例中的方法赋值给一个变量,但是只是赋予了方法的引用,所以当变量在执行方法的时候,其实改变了方法的执行时的上下文。原来执行的上下文是实例cat,后来赋值之后再执行,上下文就变成了全局,this默认绑定。class中使用的是严格模式,在该模式下,全局的this默认绑定的是undefined,不是在严格模式下的时候,若在浏览器中执行,则this默认绑定window

jump方法: 因为在构造函数执行的时候,显式绑定了jump执行的上下文cat实例。所以jump的执行上下文依然是cat实例

go方法: go方法使用静态方法定义,无法共享实例cat,只能在构造函数Cat上直接调用。

walk与drink方法: 这两个方法是用箭头函数定义的。箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。walk是在Cat.prototype.walk定义时的,此时的this指向是window。无论之后赋值给哪个变量,也只是用函数的引用,所以其this还是window。同理,drink在定义的时候,this指向的是该构造函数。

注意点:

(1)严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

(2)this的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
复制代码

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错。

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  // ...
}
复制代码

另一种解决方法是使用箭头函数。

class Obj {
  constructor() {
    this.getThis = () => this;
  }
  getVal = () => this;
}

const myObj = new Obj();
myObj.getThis() === myObj // true
myObj.getVal() === myObj // true
复制代码

箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。

(3)静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就被称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
复制代码

上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}

Foo.bar() // hello
复制代码

上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名

父类的静态方法,可以被子类继承。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'
复制代码

上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

静态方法也是可以从super对象上调用的。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"
复制代码

参考文档:

es6.ruanyifeng.com/#docs/class

developer.mozilla.org/zh-CN/docs/…

文章分类
前端
文章标签