前言: 本来只是想写一下简单的 bind 函数实现,没想到写着写着还能牵出 js 中继承的知识,其实研究原生函数的实现总是能学到很多新东西
在实现之前呢,我们首先要知道 bind
是做什么的。JS MDN 给出的定义是 The bind() methods creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called. ,简单来说就是 bind()
函数创建了一个新函数(原函数的拷贝),这个函数接受一个提供新的 this
上下文的参数,以及之后任意可选的其他参数。当这个新函数被调用时,它的 this
关键字指向第一个参数的新上下文。而第二个之后的参数会与原函数的参数组成新参数(原函数的参数在后),传递给函数。
弄清楚这个之后,我们再来分析看看要怎么做。
首先,调用 bind() 会返回一个闭包,这个闭包中创建了一个新函数,这个函数首先包含原函数的属性与方法,并且这个函数的 this 值是传给 bind() 函数第一个参数,所以自然而然我们想到用 call 或者 apply 来改变原函数的 this ,这里我们选择 apply, 理由是我们新函数的第一个之后的参数是由传给 bind() 的第二个及之后的参数(代码中的 formerArgs )再加上原函数的参数(代码中的 laterArgs )构成的,我们把它们拼接成一个数组就完事儿了~具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function .prototype.bind = function (ctx ) {
var _this = this
var slice = Array .prototype.slice
var formerArgs = slice.call(arguments , 1 )
return function ( ) {
let laterArgs = slice.call(arguments , 0 )
return _this.apply(ctx, formerArgs.concat(laterArgs))
}
}
ES6 实现 上面的代码是基于 ES5 实现的,当时对于不确定的参数的一般处理方法都是利用类数组对象 arguments
(其中包含了传递给函数的所有参数),也就免不了使用 call
或者是 apply
对其进行数组操作。代码也就显得比较冗长。但是 ES6 不一样了呀~我们有了不定参数 这个神器。无论有无参数,有几个参数都可以简单地处理。
不定参数: 传递给函数的最后一个参数可以被标记为不定参数,当函数被调用时,不定参数之前的参数都可正常被填充,剩下的参数会被放进一个数组中,并被赋值给不定参数。而当没有剩下的参数时,不定参数会是一个空数组,而不会被填充为 undefined
。
同样的功能,只要几行代码就可以实现:
1
2
3
4
5
6
7
8
9
10
Function .prototype.bind = function (ctx, ...formerArgs ) {
let _this = this
return (...laterArgs ) => {
return _this.apply(ctx, formerArgs.concat(laterArgs))
}
}
至此,我们就实现了简单的 bind() 函数的功能,接下来我们给它做点优化。
优化 upupup..
当 Function 的原型链上没有 bind 函数时,才加上此函数
1
2
3
if (!Function .prototype.bind) {
}
只有函数才能调用 bind 函数,其他的对象不行。即判断 this 是否为函数。
1
2
3
if (typeof this !== 'function' ) {
}
压轴戏: 关于继承 我们上面的代码使用了借用 apply 继承的方式。用了 apply 来改变 this 的指向,继承了原函数的基本属性和引用属性,并且保留了可传参优点,但是新函数无法实现函数复用,每个新函数都会复制出一份新的原函数的函数,并且也无法继承到原函数通过 prototype 方式定义的方法或属性。
为解决以上问题,我们选用 组合继承 方式,在使用 apply 继承的基础上,加上了原型链继承。 所以我们可以这么改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (!Function .prototype.binds) {
Function .prototype.binds = function (ctx ) {
if (typeof this !== 'function' ) {
throw new TypeError ("NOT_A_FUNCTION -- this is not callable" )
}
var _this = this
var slice = Array .prototype.slice
var formerArgs = slice.call(arguments , 1 )
var fun = function ( ) {}
var fBound = function ( ) {
let laterArgs = slice.call(arguments , 0 )
return _this.apply(ctx, formerArgs.concat(laterArgs))
}
fun.prototype = _this.prototype
fBound.prototype = new fun()
return fBound
}
}
在上面的代码中,我们引入了一个新的函数 fun,用于继承原函数的原型,并通过 new 操作符实例化出它的实例对象,供 fBound 的原型继承,至此,我们既让新函数继承了原函数的所有属性与方法,又保证了不会因为其对原型链的操作影响到原函数。用图来表示应该是下面这样的:
这样我们对新函数的 prototype 修改只会应用在它自己身上,而不会影响到原函数。
其他 MDN 中还提到,若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象。所以我们最终的代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (!Function .prototype.binds) {
Function .prototype.binds = function (ctx ) {
if (typeof this !== 'function' ) {
throw new TypeError ("NOT_A_FUNCTION -- this is not callable" )
}
var _this = this
var slice = Array .prototype.slice
var formerArgs = slice.call(arguments , 1 )
var fun = function ( ) {}
var fBound = function ( ) {
let laterArgs = slice.call(arguments , 0 )
return _this.apply(this instanceof fun ? this : ctx || this , formerArgs.concat(laterArgs))
}
fun.prototype = _this.prototype
fBound.prototype = new fun()
return fBound
}
}
打完收工~~欧耶 ( •̀ ω •́ )y
P.S: ES7 中已经淘汰了 .bind 的写法,而是使用两个冒号的方式 :: 来代替,(要不要这么可爱!不过呢,人家还只是个提案而已,到时候会不会被采用还不一定的呢
用法有两种,第一种 ::对象.方法名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {
val : 'value' ,
method : function ( ) {
console .log(this .val)
}
};
::obj.method;
obj.method();
第二种, 对象::方法名()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = {
val : 'value'
};
function method ( ) {
console .log(this .val);
}
method = obj::method;
method();
嘛~ 有说的不对的地方,欢迎指正..