复习 JavaScript: 实现 call、bind、apply

107 阅读4分钟

关于call、apply 和 bind 都是用来改变函数的 this 指向,前两者的区别只是传递参数的形式不一样,apply的第二个参数需要以数组的方式传入。bind则是返回了修改this后的函数。这三种方法在每天搬砖的过程中都会被高频率的使用,让我们试试手动来实现一下吧。

call :

实现

Function.prototype.myCall = function (context) {
  let obj = Object(context) || window //在没有传入修改后的上下文对象时,模式是全局上下文 window (ps:浏览器中)
  let fn = Symbol() // 创建一个唯一标识
  obj.fn = this // 将新建空对象上的属性赋值为将要执行的函数
  let res = obj.fn(...[...arguments].splice(1)) // 将除上下文对象的参数传递到函数内执行
  delete obj.fn // 删除新建的属性
  return res // 当函数存在返回值时,返回对应值
}

var a = 1, b = 2;
var obj ={a: 10,  b: 20}
function test(key1, key2, key3){
  console.log(this[key1] + this[key2]) 
}
test('a', 'b') // 3
test.myCall(obj, ['a', 'b']) // 30

apply :

关于 apply 的实现实际上和 call 区别不大,只在参数的传递上存在区别。

Function.prototype.myApply = function (context) {
  let obj = Object(context) || window
  let fn = Symbol()
  obj.fn = this
  let arg = arguments[1].length ? [...arguments[1]] : [] // 传参时的区别
  let res = obj.fn(...arg)
  delete obj.fn
  return res
}

var a = 1, b = 2;
var obj ={a: 10,  b: 20}
function test(key1, key2){
  console.log(this[key1] + this[key2]) 
}
test('a', 'b')
test.myApply(obj, ['a', 'b']) 

bind:

bind与前两者的区别就是,返回了修改this后的函数并不会立即执行。

Function.prototype.myBind = function (context) {
  let obj = Object(context) || window
  let fn = Symbol()
  obj.fn = this
  let arg = [...arguments].splice(1)
  return function () {
    let res = obj.fn(...arg, ...arguments)
    delete obj.fn
    return res
  }
}

var a = 1, b = 2, c = 3;
var obj ={a: 10,  b: 20, c: 30}
function test(key1, key2, key3){
  console.log(this[key1] + this[key2] + this[key3]) 
}
test('a', 'b', 'c') //6
test.myBind(obj, 'a', 'b')('c') //60

就这么结束了???too young...too simple...

function Foo (name, age) {
  this.name = name
  this.age = age
}

obj = {
  name: 'lalala',
  age: 2
}

name = 'heiheihei'
age = 3

console.log(new Foo(name, age)) // heiheihei 3

let fn1 = Foo.bind(obj, name, age)
console.log(new fn1(name, age)) // heiheihei 3
let fn2 = Foo.myBind(obj, name, age)
console.log(new fn2(name, age)) // {}

bind是会返回修改this后的函数,所以就存在修改后的函数被当做构造函数的可能。在补全这部分功能之前先了解new做了什么。

new 运算符

内部操作

查找资料后发现 new 的内部操作:

  • 创建一个空的简单 JavaScript 对象;
  • 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  • 将新创建的对象作为this的上下文 ;
  • 如果该函数没有返回对象,则返回新创建的对象;

实现

function MyNew (fn) {
  // 使用 Object.create 创建一个空对象并把新对象的 __proto__ 指向 fn 的原型
  // 这样才会将构造函数实例的方法同步到新的对象中,例如👇实例中 person1.getName
  let obj = Object.create(fn.prototype) 
  // 不使用 Object.create 
  //let obj = {}
  //obj.__proto__ = fn.prototype
  
  // 收集需要传递的参数
  let args = [...arguments].splice(1)
  
  // 将新建对象作为函数执行的上下文  
  // 这步操作就是让新建的对象会有函数中操作的属性值,例如👇示例中的 person1
  let res = fn.apply(obj, args) 
  
  // 当函数的返回值为一个对象或一个函数时返回对应值;反之返回新建的对象。
  return typeof res === 'object' || typeof res === 'function' ? res : obj
}

function Person(name, age){
  this.name = name;
  this.age = age;
}
Person.prototype.getName = function() {
  return this.name
}

var person1 = MyNew(Person, '张三', 18);
var person2 = MyNew(Person, '李四', 18);
console.log(1111, person1) // 张三 18
console.log(222, person1.getName()) // 18
console.log(1111, person2) //李四 18
console.log(2222, person2.getName()) // 18

关于 Object.create()

语法

Object.create(proto,[propertiesObject])

  • proto 新创建对象的原型对象
  • propertiesObject 可选。需要传入一个对象,该对象的属性类型参照Object.defineProperties()的第二个参数。如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。

与 new 和 字面量新建对象的区别

字面量和new关键字创建的对象是 Object 的实例,原型指向Object.prototype,继承内置对象Object; Object.create(arg, pro)创建的对象的原型取决于argargnull,新对象是空对象,没有原型,不继承任何对象;arg为指定对象,新对象的原型指向指定对象,继承指定对象;

理解

function create ( obj ) {
    function f(){}
    f.prototype = obj
    return new f
}

// 更简单的理解~~
function create(obj) {
    return {
        __proto__: obj
    }
}

bind 实现重写

Function.prototype.myBind2 = function (context) {
  let fn = this
  let arg = [...arguments].splice(1)
  let fnForReturn = function () {
    // 判断是否被 new 调用,也可以通过 ES6 中的 new.target 进行判断;
    // 当作为构造函数时,this 指向的是 new 之后的实例;
    if (this instanceof fnForReturn) {
      return fn.apply(this, [...arg, ...arguments])
    } else {
      return fn.apply(context || window, [...arg, ...arguments])
    }
  }
  // 将返回函数的实例赋值为初始被 bind 的函数实例,实例可以继承绑定原函数的值
  fnForReturn.prototype = this.prototype
  return fnForReturn
}

//---------------------------------测试一下效果👇--------------------------------------
function Foo (name, age) {
  this.name = name
  this.age = age
}

obj = {
  name: 'lalala',
  age: 2
}

name = 'heiheihei'
age = 3

console.log(new Foo(name, age)) // heiheihei 3

let fn1 = Foo.bind(obj, name, age)
console.log(new fn1(name, age)) // heiheihei 3

let fn2 = Foo.myBind2(obj, name, age)
console.log(new fn2(name, age)) // heiheihei 3

以上是自己在复习时的思路和简单实现,如果有哪里有问题还希望大佬们指正~~