乐趣区

关于前端:从理解到实现JS中的callapplybind

写在后面:看似简略的一个 JS 的小办法,但从一直的开掘中扩大了常识的深度和广度,除此之外,也是练习并把握了一个学习办法,进步学习能力。从应用到了解,再到尝试实现,一直去试错,反复去浏览,最初把脑中的了解转换成文字。

参考文章:
https://segmentfault.com/a/11…
https://juejin.im/post/5bec41…
http://yanhaijing.com/es5/#238
https://segmentfault.com/q/10…
https://blog.csdn.net/wxl1555…
https://www.w3cschool.cn/java…
https://www.bbsmax.com/A/lk5a…

1. call 办法

1.1 概述

参数解释:

function.call(thisArg, arg1, arg2, ...)

  • thisArg: 在 fun 函数运行时指定的 this 值。须要留神的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 nullundefinedthis 值会主动指向全局对象 (浏览器中就是 window 对象),同时值为原始值(数字,字符串,布尔值) 的this会指向该原始值的主动包装对象。
  • arg1, arg2, ...: 指定的参数列表。
var obj = {name:'CSDN'};
function fn() {// console.log(this);
    console.log(this.name);
}
fn();          // undefined
fn.call(obj);  // CSDN

了解:首先寻找 call 办法,通过原型链的查找,在 Function.prototype 上找到 call 办法;而后,扭转 fn 函数中的 this 指向,将 fn 执行。

var obj = {name:'zl'};
function fn(age, country) {console.log(this.name + '-' + age + '-' + country);
}
fn.call(obj, 18, 'China'); // zl-19-China

了解:带参数传入,参数须要开展,这也是惟一于 apply 办法不同的中央。

1.2 模仿 call 办法的原理去了解 this

var obj = {age: 18}
function fn1 () {console.log(this);
      console.log('fn1');
}
Function.prototype.myCall = function (arguments) {
    // 1. 解构传递进来的参数 arguments
    //    obj(须要扭转的指标指向)arg1,arg2,arg3..(其余参数)// 2. 扭转 fn1 函数中的 this 指向 obj
    // 3. 此函数体(myCall)中的 this 指向 fn1
    //    将 arg1,arg2..(其余参数)传入 fn1 办法中执行 
}
fn1.myCall(obj, arg1, arg2, arg3...);
// myCall 办法中原来的 this 是 obj
// 输入:{age: 18}   fn1

fn1.myCall(obj)在执行的时候,首先扭转的是 fn1 函数中的 this 指向为指标对象,而 myCall 办法中的 this 是指向 fn1 的。所以,最初执行的时候也是执行 myCall 办法中的 this 所指向的函数。

var obj = {};
var f = function () {return this;};
f() === this // true
f.call(obj) === obj // true

下面代码中,在全局环境运行 f 时,this指向全局环境;call办法能够扭转 this 的指向,指定 this 指向对象 obj,而后在对象obj 的作用域中运行函数f

function f1 () {console.log('f1')
    // console.log(this) 输入 [Function: f2]
}
function f2 () {console.log('f2')
}
f1.call(f2) // f1

上例再次很好的阐明了:call办法只会扭转 f1this指向,而不是扭转本身的 this 指向,最初执行的函数,只会是本身的 this 指向——调用 callf1,而不是f2

1.3 总结

  • call办法的第一个参数用于扭转 调用 call 办法的函数内,this的指向 ,然而如果传入null/undefined 值,此 this 会指向window
  • call办法须要把实参依照形参的个数传进去
  • call办法最初会应用参数去执行 call 函数体内 this 所指向的函数,个别是指向 调用 call 的函数

1.4 深刻了解 call 的小例子

function f1 () {console.log('f1')
}
function f2 () {console.log('f2')
}
function f3 () {console.log('f2')
 }
 f1.call(f2)                     // f1
 f1.call.call(f2)                // f2
 f1.call.call.call(f2)           // f2
 f1.call.call.call(f3)           // f3

要了解 f1.call.call(f2),首先拆分它,先看f1.call,在Function.prototype 上找到 call 办法,只实现——call函数体中的 this 指向 f1(没有须要 f1this扭转的指标对象)。所以当初能够将 f1.call 看成 一个函数(func),再次强调此函数中的 this 指向f1

// f1.call 能够写成如下
Function.prototype.call = function (obj) {
    // 1. 没有扭转 f1 中 this 的指向
    // 2. 此函数体内 this 指向 f1
    this() // 指向 f1}

Func = Function.prototype.call
// function Func () {
    // ... 省略代码
    this() // 指向 f1}

当初 f1.call.call(f2) 能够看成是 func.call(f2)func 调用 call 办法,首先扭转 functhis的指向为 f2,而后执行调用了call 的函数 funcfunc 就是之前的 f1.call),而这个函数内,this 指向f2,所以这里输入‘f2’

Function.prototype.call = function (obj) {
   // 1. 扭转 Func 中 this 的指向为 f2
   // 2. 执行 this()
   this()  // 指向 Func}

其余例子可照此法去了解,可能明确 call 扭转的是谁的this,最初执行的又是哪个函数。

1.5 入手仿写一个 call 办法

之前咱们曾经晓得了 call 外部都做了什么,接下来通过代码来实现它的性能:

  • call办法接管的第一个参数应该是一个对象,非严格模式下,如果为空、null、undefined,则默认传入全局对象。
  • call办法扭转函数外部的 this 指向,并在指定作用域中调用该函数
// node 环境下输出 love = 'global love' 
// window 浏览器中运行输出 var love = 'window love'
var love = 'window love'
Function.prototype.myCall = function () {// console.log(arguments);
  // 输入:{'0': Object { age: 19, love: "sleeping"}, '1': 23, '2': 25, ...}
  var [thisArg, ...args] = [...arguments]
  if(!thisArg) {thisArg = typeof window === undefined ? global : window}
  thisArg.fn = this
  let res = thisArg.fn(...args)
  delete thisArg.fn
  return res
}
var zhou = {
  age: 18,
  love: 'coding',
  hello: function (age) {console.log("hello world, i am zhou," + age + "," + this.love);
  }
};
var wang= {
  age: 19,
  love: 'sleeping'
};
zhou.hello.call();
// hello world, i am zhou,undefined window love
zhou.hello.myCall(wang, 23, 25, 90, 8)
// hello world, i am zhou,23 sleeping

从代码 log(arguments) 中,咱们晓得了 myCall 办法中 arguments 的构造:第 0 个元素对象正是咱们须要批改 this 的指标对象,其余元素则是传递的实参。if语句是为了更好的欠缺 myCall 的性能,依据代码运行环境,全局对象的 this 的指向是不同的,具体参考此文。

要害是 thisArg.fn 的了解:myCall中的 this 指向的是 zhou.hello 这个办法;给 thisArg 增加一个 fn 的属性(就是办法 zhou.hello)。而thisArg.fn(...args) 能够看成 zhou.hello(23, 25, 90, 8),而zhou.hello 只承受一个参数,天然就是第一个 23 了。最初通过 delete 删除自增加的属性this.fn

1.6 补充

理解了 call 办法的作用原理,最初看一个 es5 的对于把类数组转换为数组的办法:
Array.prototype.slice.call(arguments)

function test () {var res = Array.prototype.slice.call(arguments, 1,3)
  console.log(res); // ['bbb', 123]
}
var a = 'aaa', b = 'bbb', c = 123, d = 'ddd'
test(a,b,c,d)

slice是数组才有的切割数组的办法,arguments是典型的类数组对象。这里先是调用 Array.prototype 上的 slice 办法,而这个办法必定是有 call 办法的,依据其作用原理,将 Array.prototype.slice 的作用域改为arguments,最初执行返回一个数组。

2. apply 办法

2.1 概述

调用一个具备给定 this 值的函数,以及作为一个数组(或相似数组对象)提供的参数。

func.apply(thisArg, [argsArray])

  • thisArg: 可选的。在 func 函数运行时应用的 this 值。请留神,this可能不是该办法看到的理论值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会主动替换为指向全局对象,原始值会被包装。
  • argsArray:可选的。一个数组或者类数组对象,其中的数组元素将作为独自的参数传给 func 函数。如果该参数的值为 nullundefined,则示意不须要传入任何参数。

2.2 举例说明 call 和 apply

// 简略的
let arr = [1, 2, 3]
let obj = {name: 'obj inner'}
function test (one, two, three) {console.log(one, two, three)
    console.log(arguments)
    return this.name
}
console.log(test.apply(obj, arr))
// 1 2 3
// [Arguments] {'0': 1, '1': 2, '2': 3}
// obj inner

// 求数组中的最大值 
var arr = [2, 7, 10, 1]
function getMax2(arr) {return Math.max.apply(null, arr);
  //return Math.max.call(null, ...arr);  
}
console.log(getMax2(arr));  //10
alert(Math.max(1,4,9,6));    //9

// 实现继承
function Animal(type, value) {
      this.type = type;
      this.value = value;
    }
function Dog(type, value) {Animal.apply(this, [type, value]);
      // Animal.call(this, type, value)
      this.name = "二狗子";
      this.age = 18
    }
var hehe = new Dog("室友", "1");
console.log(hehe.name); // 二狗子
console.log(hehe.type); // 室友

总结

  1. apply办法接管到的数组作为参数传递给 func 时,须要用多个参数来承受数组中的每一项(猜想 Math.max.apply(null, arr)apply会主动将数组转变成参数列表,其等价于 Math.max(2,7,10,1))。可应用arguments 来全副接管。
  2. 咱们能够看出 applycall的不同:接管的参数一个是数组一个是参数列表。
  3. 能够应用 applycall实现继承,办法是相似的,后果是统一的。

apply还多用于构造函数绑定:链接

2.3 实现 apply 办法

Function.prototype.apply = function () {var [thisArg, args] = [...arguments]
  // 与 call 办法的实现相相似,使用开展运算
  if(!thisArg) {thisArg = typeof window === undefined ? global : window}
  thisArg.fn = this
  let res = thisArg.fn(...args)
  delete thisArg.fn
  return res
}

3. bind 办法

3.1 概述

返回一个原函数的拷贝(也称绑定函数),在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。详见 MDN。

function.bind(thisArg[, arg1[, arg2[, ...]]])

  • thisArg: 调用绑定函数时作为 this 参数传递给指标函数的值。如果应用 new 运算符结构绑定函数,则疏忽该值。当应用 bindsetTimeout中创立一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果bind 函数的参数列表为空,执行作用域的 this 将被视为新函数的thisArg
  • arg1, arg2, ...: 当指标函数被调用时,事后增加到绑定函数的参数列表中的参数。

举例说明:

 window.value = 3;
 var foo = {value:1};
 function bar() {console.log(this.value);
 }
 bar();   // 3
 bar.call(foo);    //1 
 
// 指定函数 this 绑定为 foo, 产生一个新函数,之后再运行的时候,外部的 this 就是被绑定的对象
 var bindFoo = bar.bind(foo);
 setTimeout(function() {bindFoo();
 },2000)
// 2 秒后打印 1

这个例子能够很好的了解 bind 的使用:

  • bar()间接调用函数,其中的 value 指的是全副变量value = 3
  • bar.call(foo)这里应用 call 立即扭转了 bar 中的 this 指向为 foo
  • bind 罕用于异步,在 setTimeout 中,设置的工夫内,barthis 保留着指向foo,所以两秒后打印 1,不是 3。

3.2 call,apply 与 bind 的不同

  1. bind()能够为指标函数保留 this 的指向,当执行指标函数时,this会指向设置的作用域。
  2. call()办法会立刻执行!bind()办法会返回函数的拷贝值,但带有绑定的上下文!须要咱们手动调用执行。

3.3 留神

bind()办法永恒扭转 this 的指向,前面再用 call() 会生效(对应上一大节的第一点):

var name='rose';
var obj={
    name:'jack',
    func:function(){console.log(this.name)
    }
}
var obj2={name:'zl'}
var func=obj.func.bind(obj);
func.call(obj2); // 无奈扭转 this 指向
// jack

3.4 实现 bind 办法

先看一个例子,明确 bind 的用法中的难点:

var obj = {name: 'out obj',};
function original(a, b){console.log('this', this); // original {}
  console.log('typeof this', typeof this); // object
  this.name = b
  console.log('name', this.name); // name 2
  console.log('this', this);  // original {name: 2}
  console.log([a, b]); // 1, 2
}
var bound = original.bind(obj, 1);
var newBoundResult = new bound(2);
console.log(newBoundResult, 'newBoundResult'); // original {name: 2}

从上例中能够理解,因为应用 newbind 原来实现 originalthis指向 obj 生效了。new bound的返回值,能够了解为是以 origin 为原型生成的 新对象 。而依据new 的性能,original中的 this 指向的就是这个 新对象

new的性能:

  1. 创立一个空对象,构造函数中的 this 指向这个空对象
  2. 这个新对象被执行 [原型] 连贯
  3. 执行这个构造函数属性和办法增加到 this 新对象中
  4. 如果构造函数中没有返回其余对象,那么就返回 this,即创立的新对象

MDN:绑定函数也能够应用 new 运算符结构,它会体现为指标函数曾经被构建结束了似的。提供的 this 值会被疏忽,但前置参数仍会提供给模仿函数。

阐明绑定函数被 new 实例化之后,须要继承原函数的原型链办法,且绑定过程中提供的 this 被疏忽(继承原函数的 this 对象),然而参数还是会应用。所以须要一个直达函数把原型链传递上来。即最终实例化之后的对象 this 须要继承自原函数

再看咱们的 mybind 办法:

Function.prototype.mybind = function () {if (typeof this !== "function") {throw new TypeError(this + 'must be a function');
  }
  let _this = this
  var [thisArg, ...args] = [...arguments]
  function fn () {let tempargs = [...arguments] 
    let newargs = args.concat(tempargs)
    _this.apply(thisArg, newargs)
  }
  return fn

返回的函数的 this 指向是固定的,在执行 mybind 的时候就曾经固定是 thisArg,并没有把原函数的this 对象继承过去。

所以在 new 新的实例的时候实时将这个新的 this 对象 进行 apply 继承原函数的 this 对象,

Function.prototype.mybind = function () {if (typeof this !== "function") {throw new TypeError(this + 'must be a function');
  }
  let _this = this
  var [thisArg, ...args] = [...arguments]
  let fTemp = function () {}

  function bound () {let tempargs = [...arguments] 
    let newargs = args.concat(tempargs)
    _this.apply(this instanceof fTemp ? this : that || window, newargs)
  }

  fTemp.prototype = _this.prototype
  bound.prototype = new fTemp()
  return bound
 }
 
var zhou = {
  age: 18,
  love: 'coding',
  hello: function (age,a,b,c,d,e) {console.log("hello world, i am zhou," + age + "," + this.love);
    console.log(a,b,c,d,e);
    
  }
};

var wang= {
  age: 19,
  love: 'sleeping'
};

let bound = zhou.hello.mybind(wang, 23, 25, 90, 8)
let a = new bound()
console.log(a); // hello {}}

重点了解这部分:

....
let fTemp = function () {}
function bound () {let tempargs = [...arguments] 
    let newargs = args.concat(tempargs)
    _this.apply(this instanceof fTemp ? this : that || window, newargs)
}

fTemp.prototype = _this.prototype
bound.prototype = new fTemp()
return bound
...

这里须要是辨别 bound 是间接调用还是被 new 之后再调用(mybind 返回的就是 bound),通过原型链的继承关系能够晓得,boud 属于 after_newnew 进去的实例)的父类,所以 after_new instanceof bound 为 true。

同时 fTemp.prototype = _this.prototypebound.prototype = new fTemp() 原型继承,使得 fTemp 也是 after_new的父类,after_new instanceof fTemp 为 true。

最初,因为 let after_new = new bound() 使得 bound 中的 this 指向的就是after_new

退出移动版