前端练习47 自动绑定实例方法

利用Proxy自动绑定实例方法

知识点

  1. class内部默认是严格模式
  2. class定义的原型方法的可枚举行
  3. Proxy

题目

首先看这样一个引子:

1
2
3
4
5
6
7
8
9
10
11
12
const name = 'window';
let obj = {
name: 'jay',
say() {
console.log(this.name)
}
};

obj.say();

let say = obj.say;
say();

两次执行,分别会输出什么?

显然,第一次会打印出jay,第二次是window(,如果在严格模式下会报错),是因为this指向调用者,第二次的调用者是window

在构建类的时候,同样有这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
constructor (name) {
this.name = name
}
sayHi () {
console.log(`I am ${this.name}.`)
}
}

const jerry = new Person('Jerry')
const sayHi = jerry.sayHi
sayHi() // => 报错

注意,这里面是直接报错,因为this是指向了undefined而不是window,因为==在类和模块的内部,默认就是严格模式==

所以在类似于React.js的组件的事件监听当中我们总是需要手动地进行bind(this)操作。为了简化这样的操作,请你完成一个方法autoBind,它可以接受一个类作为参数,并且返回一个类。返回的类的实例和原来的类的实例功能上并无差别,只是新的类的实例所有方法都会自动 bind 到实例上。例如:

1
2
3
4
5
const BoundPerson = autoBind(Person)

const lucy = new BoundPerson('Lucy')
const sayHi = lucy.sayHi
sayHi() // => I am Lucy.

注意,如果autoBind以后给原来的类新增方法,也会自动反映在实例上,例如:

1
2
3
4
5
6
Person.prototype.sayGood = function () {
console.log(`I am ${this.name}. I am good!`)
}

const sayGood = lucy.sayGood
sayGood() // => I am Lucy. I am good!

实现

一上来我就想到了可以使用Proxy来拦截new的操作符,后来又试了试用extends方法,都可以实现一半,但是后一半,也就是“自动反映在实例上”这个要求没办法实现

先讲一下我用Proxy的实现,利用handler.construct方法,拦截了new的操作

1
2
3
4
5
6
7
8
9
10
11
const autoBind = (ToBindClass) => new Proxy(ToBindClass, {
construct(targets, argus, newTarget) {
const self = new targets(...argus);

Object.getOwnPropertyNames(targets.prototype).forEach(v => {
self[v] = targets.prototype[v].bind(new targets(...argus))
});

return self
}
})

在构造函数的方法中,使用了Object.getOwnPropertyNames(targets.prototype)对原型上的方法遍历进行绑定

要注意的是,==class里面定义的原型方法是不可枚举的==,这一点与直接定义在prototype是不同的,直接定义在prototype是可枚举的

所以遍历class的原型方法不能使用Objecet.keys方法,可以使用Object.getOwnPropertyNames

但是,这种方法没办法动对新增的方法进行处理。

看了看评论区,可以使用双层的Proxy实现,外层的Proxy不同,里面不直接返回实例,而是用Proxy再次进行处理的实例,对实例的get方法进行拦截,每次访问的时候再去原型链上找,然后进行绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const autoBind = (ToBindClass) => new Proxy(ToBindClass, {
construct(targets, argus, newTarget) {

const self = new targets(...argus);

return new Proxy(self, {
get(target, key) {
if(typeof target[key] === 'function') {
target[key] = target[key].bind(self)
}
return target[key]
}
})
}
})

外层的Proxy可以用函数自己去拦截构造器,所以上面也可以改写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
const autoBind = (ToBindClass) => {
return function(...argus) {
const self = new ToBindClass(...argus);
return new Proxy(self, {
get(target, key) {
if(typeof target[key] === 'function') {
target[key] = target[key].bind(self)
}
return target[key]
}
})
}
}

太笨了。

参考