一文学会js里bind方法的模拟实现,掌握bind、apply和bind区别和用法
你越是认真生活,你的生活就会越美好
——弗兰克·劳埃德·莱特
《人生果实》经典语录
bind
一句话介绍 bind:
bind() 方法
会创建一个新函数
。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的this
,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )
由此我们可以首先得出bind 函数
的两个特点:
- 返回一个函数
- 可以传入参数
返回函数的模拟实现
从第一个特点开始,我们举个例子:
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
// 返回了一个函数
var bindFoo = bar.bind(foo);
bindFoo(); // 1
关于指定 this 的指向
,我们可以使用 call 或者 apply 实现
。
我们来写第一版的代码:
// 第一版
Function.prototype.bind2 = function (context) {
var self = this;
return function () {
return self.apply(context);
}
}
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
// bar得是一个函数 在可以在原型链上找到bind2方法
var bind2Foo = bar.bind(foo);
bind2Foo(); // 1
之所以 return self.apply(context)
,是考虑到绑定函数可能是有返回值的
传参的模拟实现
接下来看第二点,可以传入参数。这个就有点让人费解了,我在 bind 的时候,是否可以传参呢?我在执行 bind 返回的函数的时候,可不可以传参呢?让我们看个例子:
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18
函数需要传 name 和 age
两个参数,竟然还可以在 bind 的时候,只传一个 name,
在执行返回的函数的时候,再传另一个参数 age!
我们用 arguments 进行处理
// 第二版
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}
构造函数效果的模拟实现
完成了这两点,最难的部分到啦!因为 bind 还有一个特点,就是
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
也就是说当 bind 返回的函数作为构造函数
的时候,bind 时指定的 this 值
会失效,但传入的参数依然生效。举个例子:
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind
,说明绑定的 this 失效了
,如果大家了解new 的模拟实现
,就会知道这个时候的 this 已经指向了 obj。
所以我们可以通过修改返回的函数的原型来实现,让我们写一下:
// 第三版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fBound.prototype = this.prototype;
return fBound;
}
构造函数效果的优化实现
但是在这个写法中,我们直接将 fBound.prototype = this.prototype
,我们直接修改 fBound.prototype
的时候,也会直接修改绑定函数的 prototype
。
这个时候,我们可以通过一个空函数来进行中转
:
// 第四版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
到此为止,大的问题都已经解决,给自己一个赞!o( ̄▽ ̄)d
调用 bind 的不是函数咋办?
抛出错误
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
最终代码
所以最最后的代码就是:
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
// 下面这样调用时 就会抛出错误
Function.prototype.bind2.call('test') // Uncaught Error: Function.prototype.bind - what is trying to be bound is not callable
细说 call、apply 以及 bind 的区别和用法
call 和 apply 的共同点
它们的共同点是,都能够改变函数执行时的上下文
,将一个对象的方法借给另一个对象来使用
,并且立即执行
。
调用 call 和 apply 的对象,必须是一个函数 Function
call 和 apply 的区别
它们的区别,主要体现在参数的写法上。先来看一下它们各自的具体写法。
call的写法
Function.call(obj, a,b,c,...)
需要注意以下几点:
- 调用 call 的对象,必须是个
函数 Function
。 call
的第一个参数,是一个对象。 Function 的调用者,this将会指向这个对象
。如果不传,则默认为全局对象 window。- 第二个参数开始,
可以接收任意个参数
。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
function func (a,b,c) {}
func.call(obj, 1,2,3)
// func 接收到的参数实际上是 1,2,3
func.call(obj, [1,2,3])
// func 接收到的参数实际上是 [1,2,3],undefined,undefined
apply 的写法
Function.apply(obj,argArray)
需要注意的是:
- apply的调用者必须是函数 Function,并且
只接收两个参数
,第一个参数的规则与 call 一致。 - 第二个参数,
必须是数组或者类数组,它们会被转换成类数组
,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
func.apply(obj, [1,2,3])
// func 接收到的参数实际上是 1,2,3
func.apply(obj, {
0: 1,
1: 2,
2: 3,
length: 3
})
// func 接收到的参数实际上是 1,2,3
function bar () {
console.log(arguments)
console.log(arguments instanceof Array)
}
什么是类数组?
先说数组,这我们都熟悉。
它的特征有:
- 可以通过角标调用,如 array[0];
- 具有长度属性length;
- 可以通过 for 循环或forEach方法,进行遍历。
那么,类数组是什么呢?顾名思义,就是具备与数组特征类似的对象
。比如,下面的这个对象,就是一个类数组。
let arrayLike = {
0: 1,
1: 2,
2: 3,
length: 3
};
类数组 arrayLike
- 可以通过角标进行调用,
- 具有length属性,
- 同时也可以通过 for 循环进行遍历。
类数组,还是比较常用的,只是我们平时可能没注意到。
比如,我们获取 DOM 节点的方法,返回的就是一个类数组
。
再比如,在一个方法中使用 arguments
获取到的所有参数,也是一个类数组。
但是需要注意的是:类数组无法使用 forEach、splice、push 等数组原型链上的方法
,毕竟它不是真正的数组。
function bar () {
console.log(arguments)
console.log(arguments instanceof Array) // false
}
bar(1,2,3)
call 和 apply 的用途
下面会分别列举 call 和 apply 的一些使用场景。
call 的使用场景
- 对象的继承。如下面这个例子:
function superClass () {
this.a = 1;
this.print = function () {
console.log(this.a);
}
}
function subClass () {
superClass.call(this);
this.print();
}
subClass();
// 1
subClass
通过 call 方法
,继承了 superClass 的 print 方法和 a 变量
。此外,subClass
还可以扩展自己的其他方法。
借用方法
。还记得刚才的类数组么?如果它想使用Array 原型链上的方法
,可以这样:
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
// 上面会把类数组转化为真数组
这样,domNodes 就可以应用 Array 下的所有方法了。
apply 的一些妙用
Math.max()
。用它来获取数组中最大的一项。
const array = [1,5,2,10]
let max = Math.max.apply(null, array)
console.log(max) // 10
同理,要获取数组中最小的一项,可以这样:
const array = [1,5,2,10]
let min = Math.min.apply(null, array)
console.log(min) // 1
bind 的使用
最后来说说 bind。
在 MDN 上的解释是:bind() 方法创建一个新的函数,在调用时设置 this 关键字为提供的值。
并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
Function.bind(obj, a, b, ...)
bind 方法 与 apply 和 call 比较类似,也能改变函数体内的 this 指向。不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行
。而 apply 和 call 则是立即调用。
来看下面这个例子:
function add (a, b) {
return a + b;
}
function sub (a, b) {
return a - b;
}
add.bind(sub, 5, 3); // 这时,并不会返回 8
add.bind(sub, 5, 3)(); // 调用后,返回 8
如果 bind 的第一个参数是 null 或者 undefined,this 就指向全局对象 window。
总结
call 和 apply 的主要作用,是改变对象的执行上下文,并且是立即执行的。它们在参数上的写法略有区别。
bind 也能改变对象的执行上下文,它与 call 和 apply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。
知乎上有人分享的,关于 call 和 apply 的便捷记忆法:
猫吃鱼,狗吃肉,奥特曼打小怪兽。 有天狗想吃鱼了
猫.吃鱼.call(狗,鱼)
狗就吃到鱼了
猫成精了,想打怪兽
奥特曼.打小怪兽.call(猫,小怪兽)
猫也可以打小怪兽了
推荐阅读
文章转载于
JavaScript深入之bind的模拟实现
「干货」细说 call、apply 以及 bind 的区别和用法
谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强