theme: smartblue
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天(点击查看活动详情)
关于作用域,相信大家都不陌生,那this关键字的指向你都能清楚地分辨吗?我们今天来聊一聊this的四项绑定规则里面的显式绑定三种方法的实现原理,没错,又是干货满满的手撕源码篇,call,apply以及bind是如何实现的呢?
前言
-
当一个函数被调用时,会创建一个执行上下文
(执行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象(AO对象))
执行期上下文会记录包括函数在哪里被调用(调用栈)、函数的调用方式、传入的参数信息。this就是执行上下文中的一个属性,会在函数执行的过程中用到。 -
我们通过下面这个例子理解什么是隐式绑定?
function foo(){
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
// 此时foo函数被引用至obj这个对象中,此时foo引用有上下文对象,这时隐式绑定规则就将foo中的this定到了obj这个对象当中,即this.a和obj.a是一样的。所以打印出的this.a = 2
obj.foo() // 2
在obj对象中foo函数被引用到foo属性上,但是无论是在obj中定义还是先定义再添加为引用属性,foo函数严格来说都不属于obj对象,但是在调用的时候会使用obj的执行上下文来引用函数,这个时候可以说foo被调用时,obj对象拥有foo函数,这就是隐式绑定。即当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
知道以上几点我们就可以来实现显示绑定的三种方法了。
call
首先我们看看原生Function.prototype.call()
var name = '十九'
let obj = {
name: '寒月十九'
}
function foo(a,b) {
console.log(this.name,a + b);
return a + b;
}
foo.call(obj,2,3) // call方法会将绑定的函数调用
call方法是将foo的this指向了obj,那我们如何借助隐式绑定来实现call方法呢?
首先我们要清楚call方法接收的第一个参数是要将函数的this绑定到这个对象上,如果没有传参默认就是window
,第二个以及之后的都是函数的其他参数。
当然call方法是在Function的原型上,那么首先我们需要判断调用这个方法的是什么类型,只有function类型才能使用,并且第一个参数不传参则为window,传递了参数则用传递的参数;
if(typeof this !== 'function') {
throw new TypeError('error') // 非函数对象不能使用
}
obj = obj || window
其核心原理就是将foo隐式绑定到obj上,再调用它
obj['fn'] = this //此处的this都是指向调用call方法的函数体
const result = obj['fn'](...args)
// 由于call方法不会影响原对象,所以执行掉函数体之后,要移除掉隐式绑定
delete obj['fn']
// 最后将执行结果返回出去,这样不仅实现了this的显示绑定,而且不影响原数组
return result
但是这么写会有一些小瑕疵,就是此处我们写的是'fn'
为变量,但是如果obj原本就有key为fn的变量的话就会被重写,所以我们还需要使用Symbol
这样就不会受影响
const fn = Symbol('fn'); // 声明独一无二的变量fn
var name = '十九'
let obj = {
name: '寒月十九'
}
function foo(a,b) {
console.log(this.name,a + b);
return a + b;
}
Function.prototype.my_call = function (obj,...args) {
// 非函数不能调用
if(typeof this !== 'function') {
throw new TypeError('error')
}
obj = obj || window
const fn = Symbol('fn')
obj[fn] = this //借助隐式绑定规则
const result = obj[fn](...args)
delete obj[fn] // 不能影响原对象
return result
}
foo.my_call(obj,2,3)
console.log(obj);
这样我们就实现了call方法,call方法有一些比较实用的小技巧,比如arguments这样的类数组没有数组身上的一些api,比如slice或者splice,但是我们可以借助call方法,使得arguments可以使用。
Array.prototype.slice.call(arguments)
这样是不是我们的arguments也能使用slice方法了,原理就是显示绑定,借助数组原型上的slice方法。
apply
已经实现了call
方法的话,其实apply
方法你也已经会了,call
和apply
两个方法的最大的差别就是call
后面可以接很多个参数
,但是apply
方法第二个参数是一个数组
,也就是说将call
第二个以及之后的参数全部放到一个数组中就是apply
了
Function.prototype.my_apply = function (obj,args) {
// 非函数不能调用
if(typeof this !== 'function') {
throw new TypeError('error')
}
obj = obj || window
const fn = Symbol('fn');
obj[fn] = this
const result = obj[fn](...args)
delete obj[fn]
return result
}
以上两个方法相信都难不倒学习的伙伴们,那就直接来到最难的部分,如何实现bind
方法,因为bind方法是返回一个函数,并不像call和apply,绑定之后就会将函数调用,bind是返回一个函数体,并且可以多处传参。
bind
让我们先看看官方实现的效果
bind
调用后返回了一个函数体,当我们直接调用这个函数体并传参时,this.name
== obj.name
,但是当我们new
返回的函数时,this.name
拿到的却是undefined
,这个时候我们就需要明白new
这个方法实现了什么效果,当我们new一个构造函数时,就是让构造函数的显式原型赋给实例对象的隐式原型即
实例对象的隐式原型 === 构造函数的显式原型
手撕new
function Person(name) {
this.name = name
}
function myNew(fn,...args) {
const obj = {}
let result = fn.apply(this, args)
obj.__proto__ = fn.prototype
return typeof result === 'object' && result !== null ? result : obj
}
let p = myNew(Person, '寒月十九')
而且bind需要将foo的this绑定到obj或者是new的原型上的显示原型上,那么我们可以借助call方法来实现,当然也可以再手写一份call的实现原理,但是上面已经写过了,所以下面我会直接借用call方法。
前半部分其实和call
以及apply
大相径庭,判断调用bind
方法的是不是一个函数对象,返回一个函数体,如果bind
第一个参数不传也是默认绑定到window
上,难点就在于我们如何处理this
的指向是obj
还是foo
的原型,并且bind的参数以及bind返回出去的函数接收的参数如何处理呢?
if(typeof this !== 'function') {
throw new TypeError('error')
}
obj = obj || window
// 处理bind结接收的第二个以及之后的参数
const args = Array.prototype.slice.call(arguments,1)
const _this = this
因为在内部return
出去的函数的this
拿不到foo
,只能拿到返回出去的函数体,所以我们在foo
内部保存一个变量_this
,让我们在内部可以绑定_this
因为需要考虑到,如果bind
返回的函数体可能被new
,这样沿着原型链我们不难知道,new bar()它的this是指向foo()的,所以我们需要将foo的原型保存下来,当我们判断它是被new就将this绑定到foo,否则就绑定到obj上。
// 首先我们在bind方法内部声明一个普通函数,让其继承foo的原型
const pro = function() {}
if(this.prototype) { //
pro.prototype = this.prototype
}
为了方便原型的操作,我们将返回的函数体声明一个变量,使用instanceof方法判断是不是被new出来的,再将bind的参数以及bind返回出去的函数体的函数进行拼接。
const bound = function() {
// 判断该函数是否被new
return _this.apply(
this instanceof pro ? this : obj,
args.concat(Array.prototype.slice.call(arguments))
)
}
bound.prototype = new pro() // 原型的继承,继承foo的原型
return bound
这样一个完整的bind
就实现了,关键就在于理解new
bind返回出去的函数体
的this
的指向。
bind完整代码:
Function.prototype.my_bind = function (obj) {
if(typeof this !== 'function') {
throw new TypeError('error')
}
obj = obj || window
const args = Array.prototype.slice.call(arguments,1)
const _this = this
const pro = function() {}
if(this.prototype) {
pro.prototype = this.prototype
}
const bound = function() {
// 判断该函数是否被new
return _this.apply(
this instanceof pro ? this : obj,
args.concat(Array.prototype.slice.call(arguments))
)
}
bound.prototype = new pro() // 原型的继承,继承foo的原型
return bound
}
暂无评论内容