call/apply/bind/new模拟实现

2021/9/15 JS

关于JScallapplybind、>new函数的原生实现

# call 的模拟实现

模拟思路

  1. call改变了this的指向,指向到foo
  2. bar函数执行了
var foo = {
  value: 1,
};

function bar() {
  console.log(this.value);
}

bar.call(foo); // 1

上面可以用类似逻辑实现

  1. 将函数设置为对象的属性
  2. 执行该函数
  3. 删除该属性(函数)
Function.prototype.call2 = function(context) {
  // 首先要获取调用call的函数,用this可以获取,就是foo函数
  context.fn = this;
  context.fn();
  delete context.fn;
};

// 测试一下
var foo = {
  value: 1,
};
function bar() {
  console.log(this.value);
}
bar.call2(foo); // 1

最终版本

加了三个东西:

  • 参数
  • this参数可以传null, 此时指向的是window
  • 函数可以有返回值的
Function.prototype.call2 = function(context) {
  var context = context || window;
  context.fn = this;
  var args = [];
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push("arguments[" + i + "]");
  }
  //console.log(arguments[1]) // kevin
  //将数组的多个元素作为参数放进函数的参数里面
  //eval 会把里面的字符串解析成JavaScript代码进行执行
  //相当于 eval('context.fn(arguments[1],arguments[2])')  ===> 解析成              		  	  //context.fn(arguments[1],arguments[2])
  //args 是数组,下面这个语句会自动调用 toString的方法
  //console.log(args.toString()) // 'arguments[1],arguments[2]'
  var result = eval("context.fn(" + args + ")");
  //console.log(result) // { value: 1, name: 'kevin', age: 18 } 返回值
  delete context.fn;
  return result;
};

// 测试一下
var foo = {
  value: 1,
};
function bar(name, age) {
  console.log(this.value);
  return {
    value: this.value,
    name: name,
    age: age,
  };
}
bar.call2(foo, "kevin", 18);
// kevin
// 18
// 1

# apply 的模拟实现

applycall作用完全一样的,只不过传参数不同。apply方法传入两个参数:一个是作为函数上下文的对象,另外一个是作为函数参数所组成的数组。call方法第一个参数也是作为函数上下文的对象,但是后面传入的是一个参数列表,而不是单个数组。

Function.prototype.apply2 = function(context, arr) {
  var context = Object(context) || window;
  // console.log(context) // {value:1} Object(context)->数组转成对象
  context.fn = this;
  var result;
  if (!arr) {
    result = context.fn();
  } else {
    var args = [];
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push("arr[" + i + "]");
    }
    result = eval("context.fn(" + args + ")");
  }
  // console.log(result) // { value: 1, name: 'k', age: 'e' } 返回值
  delete context.fn;
  return result;
};

// 测试一下
var foo = {
  value: 1,
};
function bar(name, age) {
  console.log(this.value);
  return {
    value: this.value,
    name: name,
    age: age,
  };
}

bar.apply2(foo, "kevin", 18);
// kevin
// 18
// 1

# bind 的模拟实现

  • 返回一个函数
  • 可以传入参数,bind绑定的时候可以传,返回的函数在bind绑定参数的后面接着传
  • bind返回的函数是构造函数的时候,bind指定的this就会无效,但是传入的参数依旧有效
Function.prototype.bind2 = function(context) {
  if (typeof this !== "function") {
    throw new Error(
      "Function.prototype.bind - what is trying to be bound is not callable"
    );
  }
  var self = this;
  //  绑定bind的时候,传进去的参数
  var args = Array.prototype.slice.call(arguments, 1);
  // console.log(args) // [ 'daisy', 'man' ]
  var fNOP = {}; // 中转 ,避免直接修改绑定函数的prototype

  var fBound = function() {
    // 这个时候的arguments是执行函数的时候传进去的参数
    // console.log(bindArgs) // [ '18' ]
    var bindArgs = Array.prototype.slice.call(arguments);
    // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
    // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
    // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
    return self.apply(
      this instanceof fNOP ? this : context,
      args.concat(bindArgs)
    );
  };
  // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
  // 原:fBound.prototype = self.prototype;
  // 现
  fNOP.prototype = self.prototype;
  fBound.prototype = new fNOP();
  return fBound;
};

var foo = {
  value: 1,
};

function bar(name, age, sex) {
  // console.log(this.value);
  // console.log(name);
  // console.log(age);
}

var bindFoo = bar.bind2(foo, "daisy", "man");
bindFoo("18");
// 1
// daisy
// 18

# new 模拟实现

function Otaku(name, age) {
  this.name = name;
  this.age = age;
  this.habit = "Games";
  return "something";
}

Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function() {
  console.log("I am " + this.name);
};
function objectFactory() {
  var obj = new Object(),
    Constructor = [].shift.call(arguments); // shift 返回删除的数组第一个元素 也就是构造函数
  // console.log(arguments) // { '0': 'Kevin', '1': '18' }shift会修改原数组、
  // 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
  obj.__proto__ = Constructor.prototype;
  // 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
  // Constructor.apply(obj, arguments);
  // return obj;
  // +  构造函数有返回值的时候。判断返回的值是不是一个对象,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么
  var ret = Constructor.apply(obj, arguments);
  // console.log(obj) // Otaku { name: 'Kevin', age: '18', habit: 'Games' }
  return typeof ret === "object" ? ret : obj;
}
var person = objectFactory(Otaku, "Kevin", "18");
console.log(person.name); // Kevin
console.log(person.habit); // Games
console.log(person.strength); // 60
person.sayYourName(); // I am Kevin