前言
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
指的是Fo
o类,而不是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/…