# 剖析call、apply、bind
# call
TIP
实现方式可能不是最好的,欢迎指正
# 草稿版
# 原生call回顾
- 在实现之前我们先来看一下原生的call方法是如何使用,这个很重要,如果对原生的call理解不够透彻,对后面我们要自己实现会有一点障碍
let husky = {
  name:'husky',
  print:function(age){
    console.log(this.name,age);
  }
}
let keji={
  name:'keji'
}
husky.print.call(hh,5) // 输出:keji 5
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 可以看到这里输出的是keji 5而不是husky 5是因为我们改变了this指向
- OK 回顾之后,我们开始本次实验
# 草稿版开始
- 先定义一个$call方法,用来模拟call方法
- 接下来实现一个方便理解的版本,上代码,先看代码,在讲解
function $call() {
  // 第一行
  let target = arguments[0]
  let currentFn = Symbol('$call')
  target[currentFn] = this;
  let args = [...arguments].slice(1)
  return target[currentFn](...args)
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 开始讲解
# 第一行
  let target = arguments[0]
1
- 第一行很简单,就是通过arguments的方式来拿参数,arguments是一个类数组类型,具体什么是类数组,先不过多讨论,在这里我们就把他当作数组
- 此时第一个参数就是this改变指向后的对象,后面这里会有优化
  let currentFn = Symbol('$call')
1
# 第二行
- 第二行代码,其实就是声明一个变量,一个不会重复的变量,作用就是保证不会影响其他对象的属性
- 如果对Symbol还不了解的小伙伴可以先搜一下,再继续,保证后面逻辑的通畅性,还是要了解一下,不用深入了解特性就好
  target[currentFn] = this;
1
# 第三行
- 第三行代码,可以看到第一行和第二行声明的变量在这里就用上了
- 先说等号左边的代码target[currentFn]这种写法,就是给target一个currentFn属性很好理解
- 接下来就是等号右边这个this是个啥东西,在还没有写这篇文章之前,每次看到这里,都很不理解,只好背下来,但是容易忘
- 为了好理解这个this是什么,我们也不从理论上来理解,来个实验,但是最后还是要落实到理论上的
let test = {
  attr1:'属性一',
  call:function(){
    console.log(this);
  }
}
test.call() // {attr1: "属性一", call: ƒ}
1
2
3
4
5
6
7
2
3
4
5
6
7
- 可以看到我们这里定义了一个test对象,给他定义了一个call方法,在call方法中来打印日志,打印这个this
- 这个this输出的是{attr1: "属性一", call: ƒ},我们可以理解为,谁调用了这个call,这个this就是谁
- 好按照这个方式理解,我们的第三行代码中等号右边的this其实就是print这个函数(函数也是对象)
  husky.print.call(hh,5)
1
- 相信到这里,可能理解了这个this,还有其他???,为什么print函数会有call方法,这里先记下,在第五行之后讲
- 一定要记得,其实对原型链掌握很好的小伙伴应该不会有这个疑问
# 第四行
let args = [...arguments].slice(1)
1
- 第四行代码,也很好理解,对在$call中传递的参数,除了第一个参数(在之前第一行已经处理了),保存到变量args中
- 用[...arguments]的方式来处理参数,是因为arguments是类数组,之前提到过,他的原型中没有数组的很好用的工具方法
- slice(1)很简单,就是截取数组,从索引1的位置一直截取到最后一个元素
# 第五行
  return target[currentFn](...args)
1
- 这个就简单了,就是调用target的currentFn方法,并且传入参数
- return就相当于返回- target[currentFn]调用后的返回值
# 核心逻辑完成,接下来是组装环节
- 在第三行代码留了一个?没有说,接下来解惑,解涉及到原型链
- 我们先随便写一个函数,并打印它,看一下他的原型
  console.dir(function(){});
1
 
 - 可以看到其实call方法是在该方法原型链上,所以它可以调用call方法,具体原型链分析事先了解,后续我也会单独开一篇文章讲一下
- 那么在组装之前还要了解一下Function构造函数,我们再看一个示例
  console.dir(new Function());
1
 
 - 根据这个示例,其实在声明function(){}这个函数的时候,就相当于执行了new Function()
- OK根据这些特性,我们得到了思路,把$call方法挂载到Function构造函数的原型上即可
  Function.prototype.$call = $call
1
- 此时我们自定义的$call方法就挂载到了Function的原型上了
# 优化版
# $call核心逻辑优化
- 理解了之前的几行代码,我们还可以继续优化一下
- 按照习惯,上示例
let husky = {
  attr:'husky',
  print:function(age){
    console.log(this.attr, age);
  }
}
husky.print.call('test',5) // undefined 5
husky.print.call(1, 5) // undefined 5
husky.print.call(true, 5) // undefined 5
husky.print.call(Symbol(), 5) // undefined 5
husky.print.call(null, 5) // 5
husky.print.call(undefined, 5) // 5
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 可以看到当我们给call传递基本值类型的时候,原生的call方法,输出的是这些
- 所以我们要对传入的参数做一下判断
function $call() {
  let arg_1 = arguments[0];
  let target = arg_1 ? Object(arg_1) : window;
  let other_args = [...arguments].slice(1);
  let currentFn = Symbol("$call");
  target[currentFn] = this;
  return target[currentFn](...other_args);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
- 根据上面原生call函数方法执行返回值,分析出,当传入的第一个参数值为undefined或null的时候,会把target赋值为window
- 我们还要看一个实验,为什么要这么处理
function test(age) {
  console.log(this.name, age);
}
test('test') // 只输出:test 大家可以试一下
1
2
3
4
2
3
4
- 此时这个 - this就相当于是- window,- window并没有定义- name属性,看这个实验- console.log(this.name, age)中的- this.name并没有返回- undefined
- 根据这个特性,就可以解释,为什么当传入第一个参数是 - undefined或者- null要赋值为- window了
# 原型挂载优化
- 给$call方法增加属性配置,使其不可被遍历
Function.prototype.$call = $call
Object.defineProperty(Function.prototype,'$call',{
  enumerable: false
})
1
2
3
4
2
3
4
# apply
# 实现
- 其实这个方法和我们自定义的$call唯一的区别就是传入的参数,原生的apply需要传入一个数组,所以我们也要传入一个数组
- 区别二,就是需要对第二个参数进行判断是不是数组,如果不是抛出错误
 function $apply() {
  let arg_1 = arguments[0];
  
  let other_args = arguments[1] ? arguments[1] : []
  if (!Array.isArray(other_args) && other_args !== undefined)
    throw new Error("CreateListFromArrayLike called on non-object");
  let target = arg_1 ? Object(arg_1) : window;
  let currentFn =  Symbol("$apply");
  target[currentFn] = this;
  return target[currentFn](...other_args);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.$apply = $apply
Object.defineProperty(Function.prototype,'$apply',{
  enumerable: false
})
1
2
3
4
2
3
4
- ok是不是很简单,大部分逻辑不同就是在参数上
# bind
# 原生bind回顾
- 这个就和call和apply不同了,这个方法是直接返回一个改变了this指向的函数,参数可以在改变this指向的是传递,也可以在执行这个改变了this指向的函数时候再传递参数
let husky = {
  name: "husky",
  print: function (...age) {
    console.log(this.name, ...age);
  },
};
let keji = {
  name: "keji",
};
let hh = husky.print.bind(husky,'test1')
hh('test1') // husky test1 test1
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- ok,我们回顾了基础用法之后,就可以来实现
# 代码实现
- 首先我们先看代码,可以先看一下代码,带着问题,看解析,效果会更好
function $bind() {
  let target = arguments[0];
  let other_args = [...arguments].slice(1);
  let currentFn = Symbol("$bind");
  target[currentFn] = this
  
  return function(){
    let secondParams = [...arguments]
    let mergeParams = other_args.concat(secondParams)
    target[currentFn](...mergeParams)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#### 前4行代码
- 前4行代码,我们在$call中已经基本介绍了,这里就不重复讲解了
# 第5行到第9行
- 可以看到,我们这里返回了一个匿名函数,因为原生的bind也是返回一个方法,所以很容易想到,返回值是一个函数
- 接下来就要处理参数了,首先两处传参的位置,我们是很明确的,一次是bind方法中可以传入,另一次是调用返回的函数时候传入的参数,并且这两次传入参数都有效果,所以我们就要合并两次参数,第7行代码就是这个作用
- 第8行代码,就很好理解了,改变this指向,传入参数,在自定义$call中也已经分析过了
TIP
OK 到这里已经全部实现完毕了,其实重点将$call的实现研究透彻,$bind和$apply就很简单了
