写在前面
上一篇我们对于 apply 和 call 进行了一波详细解析,相信大家对于改变JavaScript的作用域使用 apply 和 call 完全没有问题了。那为什么已经有了 apply 和 call,为什么还要有 bind 呢。那下面就来和大家一起来探索一下bind。
bind 解释
Function.prototype.bind()
MDN中的解释:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
MDN 例子
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // global scope
// expected output: undefined
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42
上面这个例子就完美解释了bind的常规作用。先定义了一个module对象,里面有x和getX两个属性。然后将module对象的getX属性赋值给了unboundGetX。然后在全局调用了unboundGetX方法。注意谁调用this指向谁,这里unboundGetX在全局调用了这个方法,这里this就是全局window对象,全局并没有x属性,于是得到的是一个undefined。
下面使用bind方法改变了unboundGetX执行环境的作用域为module,这时候在执行boundGetX,这里this在module环境中,x则为42
在 MDN 中的其他例子,大家可以每个都深入看下,最好都手敲一遍,执行一下,会加深大家的理解的。
通过bind快捷调用
这个通过bind快捷调用,在看各类源码的时候,大家都会经常发现类似这种的写法,所以这里给大家分析一下
你可以用 Array.prototype.slice 来将一个类似于数组的对象(array-like object)转换成一个真正的数组
// 类数组对象
var arguments = {0:"a", 1:"b",2:"c", length:3}
var slice = Array.prototype.slice;
// 这里传入的arguments对象,是slice需要执行的作用域环境
var arr = slice.apply(arguments);
console.log(arr) // ["a", "b", "c"]
上面这个 apply 或者 call 都可以实现同样效果,因为只是传了arguments作为作用域传入,并没有其他执行的函数进去
var arguments = {0:"a", 1:"b",2:"c", length:3}
arguments.__proto__.slice = Array.prototype.slice
arguments.slice() // ["a", "b", "c"]
实际上这里类数组变成对象完全都是因为slice的设计方式上的问题,这要在继续分析就需要分析一下V8的源码了,我们暂时不继续展开,大家就上面这段代码理解下,知道实际上改变作用域环境实际上只是为了给arguments增加了一个数组才有的slice方法,只有有了这个slice方法,才能类数组对象变成数组
用 bind() 实现可以将这个
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);
slice(arguments);
这里我们知道了这样可以实现,具体分析,我们先需要看下bind的具体实现原理
实现原理
先拆分一下实现一个bind 的关键思路
- bind方法不是立即执行函数,所以需要返回一个待执行的函数。我们可以通过闭包的方式,返回一个函数( return function(){} )
- bind实现了改变方法的作用域,这里我们可以通过call和apply来实现
- 参数传递,因为bind可以在绑定方法时传递参数,也可以在调用bind之后返回的方法中进行传参。所以我们最好还是用apply了
下面我们通过几步来实现一个完整的bind
第一步 bind v1.0.0
实现作用域改变
Function.prototype.bind1 = function (context) {
var self = this
return function () {
return self.apply(context)
}
}
测试
function test () {
console.log(this.value)
}
let obj = {value: 1}
let new_bind = test.bind(obj)
new_bind() // 1
解析
这里稍微解释一下,new_bind通过执行test.bind(obj)返回的是一个待执行的方法
function () {
return self.apply(context)
}
然后调用new_bind执行这个方法的时候,我们执行的是返回的function。这里通过self将test的this保存成了self,实际就是test.apply(obj),也即是将test方法执行的作用域指向了obj。所以调用后,拿到this.value就是obj的value
第二步 bind v1.0.1
实现参数传递
Function.prototype.bind2 = function(context) {
var self = this
// 截取第一个之后的所有参数
// arguments是一个类数组的对象,有length属性
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))
}
}
测试
function test(name, age) {
console.log(this.value)
console.log(name)
console.log(age)
}
let obj = { value: 1 }
let new_bind = test.bind2(obj)
new_bind(18)
解析
这一步增加了参数传递,在bind时可以传入参数,同时执行bind之后返回的方法也可以传入参数。一开始执行bind时候,传入参数除了需要绑定的对象外,后面可以加上参数,方法中args就是bind时候传入的。bindArgs则是在调用bind返回后方法传入的,然后在apply时候,将两个参数合并
第三步 bind v1.0.2
关于bind之后的函数被new实例化
注: bind返回的新函数也能使用new操作符创建对象,这就相当于把bind之后的函数当作构造函数,实例后的函数需要继承原函数的原型链方法,并且绑定过程中的this被忽略,但是参数仍然会使用
// 这里我们增加了一个temp中转函数
// 帮助我们如果要实例化对象时,将原型链传递下去
Function.prototype.bind3 = function(context) {
var self = this,
slice = Array.prototype.slice,
args = slice.call(arguments, 1),
temp = function () {},
bound = function () {
return self.apply(context, args.concat(slice.call(arguments))
}
temp.prototype = self.prototype;
bound.prototype = new temp()
return bound;
}
使用new实例化被绑定的方法 这里不是很好理解,前面有一篇文章,我们曾经分析过new这个方法在执行的过程中干了什么,大家想详细了解可以去找下看一下。我们这里就简单在复习一下。 比如 var foo = new Foo()
// 1.创建新对象
var obj = {}
// 2.将构造函数的上的prototype属性都复制一份给obj
obj.__proto__ = Foo.prototype
// 3.将构造函数Foo的this指向当前创建的对象
Foo.call(obj)
// 4.对于构造函数可能会return对象问题做了个处理
return typeof result === 'object' ? result : obj
通过上面这个new的简单分析可以得知,如果 var bind_new = new bindFun() 这里是把原函数当作构造器,那么最终实例化后的对象this需要继承原函数。目前bindFun是
function () {
return self.apply(context || window, args.contact(Array.prototype.slice.apply(arguments,[0])))
}
这里我们apply执行的作用域是 context || window , 这在我们一开始bind3执行的时候就已经将作用域固定了,并没有把原函数的this对象继承过来。这里我们需要通过apply或者call实现一个小继承
继承详解
function Product (name,price) {
this.name = name
this.price = price
}
function Food (name, price) {
Product.call(this,name,price)
this.category = 'Food'
}
// 上面实现的继承,就是Food继承了Product,就相当于在Food内执行了一遍product,等同于下面
function Food (name, price) {
this.name = name
this.price = price
this.category = 'Food'
}
// 所以apply或者call实现的继承是不能继承原型链上的内容的
// 这部分继承知识也在之前一篇文章分析了,大家也可以去捞一捞
所以最终,我们要实现的结果就是下面这样
Function.prototype.bind4 = function(context) {
// 先判断this是不是函数,bind不是函数就要抛出异常
if(typeof this !== 'function'){
throw new Error('this must be function');
}
// 将执行bind的this存在self中
var self = this,
// 将数组的slice方法给slice
slice = Array.prototype.slice,
// 截取除了第一个后面的参数并变成数组
args = slice.apply(arguments,[1]),
// 临时方法,用于继承原函数
temp = function() {},
//最后返回的函数
bound = function () {
// 这里进行了一个this判断
// 判断是不是使用了new来使用bind返回后的函数
// 如果用了new 则 this instanceof 为 true
// 如果是直接执行bind后方法则是直接是 context
return self.apply(this instanceof temp ? this : context || window,
args.concat(Array.prototype.slice.apply(arguments,[0]))
)
}
// 将调用bind方法的方法的原型对象属性赋值给临时对象的prototype
temp.prototype = self.prototype
// 将需要返回出去的待执行方法原型对象和temp通过原型链绑定
bound.prototype = new temp()
return bound
}
例子
下面通过几个简单例子在讲解一下
var obj = {
value:1
};
function bar(name,age){
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age)
}
bar.prototype.mother = 'chencc';
var bindFoo = bar.bind(obj,'jack');
// 直接执行改变作用域绑定后的 bindFoo
bindFoo(19) // 1 jack 19
// bindFoo 是什么
// bindFoo就是上面要返回出来的 bound
// bindFoo.prototype.__proto__ === bar.prototype
// 这里就相当于上面的
// temp.prototype = self.prototype
// bound.prototype = new temp()
// bound.prototype.__proto__ === temp.prototype
var foo = new bindFoo();
// 将bind返回之后的方法当作构造器,进行实例化
// foo 就是 bound 的子类
// foo.__proto__ === bindFoo.prototype
// bindFoo.prototype.__proto__ === bar.prototype
总结
写到这里 bind 的实现原理就大概分析完了,bind的实现原理以及应用上,融合了我们前面写的call和apply解析,JavaScript在new的时候干了什么以及继承中构造器继承一些相关的东西。所以学习新的知识的时候,从来不是割裂的,而是相互都有关系的,希望大家在学习的时候,注意这些。一起提高