从工作效率来说,造轮子不是一种好的方式,但是从学习层面来说,造轮子就可以了解到更多知识。
凡是我不能创造的,都是因为我不了解它。 ———— Richard Feynman
因此,为了更好的学习call(),apply()和bind()三个JS基础内置函数,这里就来模拟实现一次。
Call 函数
首先来看一下call函数的例子:
1 |
|
运行结果是:Hello Sevens Chan
通过上面的例子,我们可以分析call函数具有以下特征:
- 原型函数的调用会改变它
this
的指向。例如:上面例子中的函数调用变成了obj.sayHello
- 无论我们传递任何参数给
sayHello.call
,都会以arg1,arg2,...
的形式传递到原始的sayHello
函数中去 - 实现的
call
函数不会污染目标函数或者传入上下文本身。
了解了基本概念,我们就可以一步一步来实现。先来实现第一个特征。
1 | Function.prototype.myCall = function (context) { |
要谨记一个法则谁调用函数,this就指向谁, 因此可以看到,我们用了传入的对象用作了当前函数的执行上下文,然后通过这个context来调用目标的函数。
接下来来实现第二条规则,参数传入:
1 | Function.prototype.myCall = function () { |
我们在网上看到很多依然使用eval
的形式的实现(当然,也有为了只使用ES3函数的前提约束的因素),而如果使用ES6的话,可以使用解构赋值,就可以很方便的把数组转换成多个参数的形式。
下面来测试下:1
2
3
4
5
6
7
8
9
10function sayHello(boyName, girlName) {
console.log(this.message, boyName, girlName)
}
const obj = {
message: 'Come on!'
}
sayHello.myCall(obj, 'Sevens', 'Pig')
// Come on! Sevens Pig
结果正确。
然后考虑一下第三点,如果传入的上下文本身就带有func
属性的话,那么这个属性值就会被我们覆盖掉了,就满足不了第三点的要求,所以做以下的改造:
1 | Function.prototype.myCall = function () { |
使用独一无二的Symbol值来做属性key,就可以保证不覆盖掉传入上下文的任何属性,达到第三点的目的。
这样一个Call函数就完成了。
apply 函数
apply和call的主要区别在于传入参数的方式不同,因此可以稍微改一下:1
2
3
4
5
6
7Function.prototype.myApply = function () {
let [context, args] = arguments
context = context || window || global
const func = Symbol()
context[func] = this
return context[func](...args)
}
同样的结果正确
bind 函数
bind函数主要作用在于更改this
的指向,看下MDN上对其的描述:
可以看到 bind
的返回值是一个绑定了this
指向的上下文的函数,并且包含了调用bind
时候传入的参数。
bind
函数创建并且返回一个new function
,叫做绑定函数。 这个绑定函数包装了原始的函数对象。
1 | Function.prototype.myBind = function(context) { |
这里的apply
可以使用我们上面实现的函数,这里就不重新实现了。
然后就是参数:1
2
3
4
5
6
7
8Function.prototype.myBind = function() {
const [context, ...args] = arguments
const boundRealFunction = this
return function boundFunction () {
const boundArgs = arguments
return boundRealFunction.apply(context, [...args, ...boundArgs])
}
}
要留意的是当返回了绑定函数之后,同样有可能在调用的时候传入参数,按照规范,这里的参数将会合并在一起。
最后有一个特性需要实现:如果绑定函数通过new
操作符来构造的话,那么bind
除第一个(context)以外的参数值将会被忽略,如下面代码:
1 | var a = 1 |
因此我们需要分辨出绑定函数是被new
来调用还是函数调用的方式调用。
回顾前面写的一篇文章 new的时候实际做了什么 可以得知通过new
创建的对象是会把新建对象的__proto__
指向绑定函数的原型对象,而在我们的myBind
实现中,它的原型对象应该就是一个function
的原型对象。
1 | Function.prototype.myBind = function() { |
最后一个特性比较难理解,需要一点时间消化。