前言
JavaScript中的this指向问题本来是一个入门必会的问题,但是对于class中this的指向问题,发现不少人还有困惑。希望这篇文章能给大家讲清楚。
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.默认绑定
在严格模式下,this是undefined,否则是全局对象。
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('豆芽')
通过上图可以看到,Cat所创建出来的实例,其方法挂载在实例的__proto__上面,即挂载在原型对象上。因为cat.proto 与 Cat.prototype指向同一个对象,所以当在cat.__proto__上挂载或者覆盖其原有方法时,所有由Cat所创建出来的实例,都将会共享该方法,所有实例都是通过__proto__属性产生的原型链到原型对象上寻找方法。
但是静态方法不会共享给实例,因为没有挂载在原型对象上面。
而属性是挂载在实例上的,即每一个创建出来的实例,都拥有自己不同值的属性。
Class中this的绑定
当我们打印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"
参考文档: