看我如何实现显示绑定


theme: smartblue

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天(点击查看活动详情)

关于作用域,相信大家都不陌生,那this关键字的指向你都能清楚地分辨吗?我们今天来聊一聊this的四项绑定规则里面的显式绑定三种方法的实现原理,没错,又是干货满满的手撕源码篇,call,apply以及bind是如何实现的呢?

快速了解javascript中的关键字_“this”’

前言

  1. 当一个函数被调用时,会创建一个执行上下文 (执行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象(AO对象))执行期上下文会记录包括函数在哪里被调用(调用栈)、函数的调用方式、传入的参数信息。this就是执行上下文中的一个属性,会在函数执行的过程中用到。

  2. 我们通过下面这个例子理解什么是隐式绑定?

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方法会将绑定的函数调用

image.png

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);

image.png

这样我们就实现了call方法,call方法有一些比较实用的小技巧,比如arguments这样的类数组没有数组身上的一些api,比如slice或者splice,但是我们可以借助call方法,使得arguments可以使用。

Array.prototype.slice.call(arguments)

这样是不是我们的arguments也能使用slice方法了,原理就是显示绑定,借助数组原型上的slice方法。

apply

已经实现了call方法的话,其实apply方法你也已经会了,callapply两个方法的最大的差别就是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

让我们先看看官方实现的效果

image.png

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

}
© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容