模拟call、apply和bind方法以及new操作符的实现

在模拟实现 call、apply 和 bind 方法以及 new 操作符的过程中,能让我们更好的理解他们的运行原理。

1、区别

call 、apply 和 bind 方法都是为了解决改变 this 的指向。

call 、apply 方法都是在传入一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。作用都是相同的,只是传参的形式不同。
除了第一个参数外,call 接收一个参数列表,apply 接受一个参数数组。使用 call 、apply 方法该函数会被执行

bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。即使用 bind 方法该函数不会被执行(返回该函数)。并且我们可以通过 bind 实现柯里化。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name, age)
    console.log(this.value)
}
getValue.call(a, 'xql', '18')
getValue.apply(a, ['xql', '18'])

getValue.bind(a, 'xql', '18') 
// getValue.bind(a, 'xql', '18')() 执行该函数

2、模拟实现 call 和 apply

  1. 先看看call 和 apply 方法发生了什么?
  • 改变了 this 的指向,指向到 a
  • getValue 函数执行了
  1. 该怎么模拟实现这两个效果呢?

既然 call 和 apply 方法是改变了 this 指向,让新的对象可以执行该函数,那么思路是否可以变成给新的对象添加一个函数(将函数设为对象的属性),然后在执行完以后删除?

Function.prototype.myCall = function(objCtx) {
    if (typeof this !== 'function') {
      throw new TypeError('Error')
    }

    let ctx = objCtx || window;
    let args = [...arguments].slice(1);
    // let args = Array.prototype.slice.call(arguments, 1)
    
    ctx.fn = this;
    let result = ctx.fn(...args);
    delete ctx.fn;
    return result; // 函数是可以有返回值的
}

// 测试一下
let a = {
    value: 1
}

function getValue(name, age) {
    console.log(this.value)
    return {
        name: name,
        age: age,
        value: this.value
    }
}

getValue.myCall(a, 'xql', '19')
getValue.myCall(null)
console.log(a)

fn 是对象的属性名,因为最后要delete,这里可以是任意名称。

以上就是 call 方法的思路,apply 方法的实现也类似。

Function.prototype.myApply = function(objCtx) {
    if (typeof this !== 'function') {
      throw new TypeError('Error')
    }

    let ctx = objCtx || window;
    ctx.fn = this; // 属性名fn可以任意
    
    let result;
    // 需要判断是否存在第二个参数
    if (arguments[1]) {
        result = ctx.fn(...arguments[1]);
    } else {
        result = ctx.fn();
    }
    delete ctx.fn;
    return result;
}

3、bind 方法的模拟实现

  1. bind 函数的两个特点?
  • 可以传入参数
  • 返回一个函数

① 其中,对于 bind 函数中的传入参数有个特点

既然在使用 bind 函数的时候,是可以传参的。那么,在执行 bind 返回的函数的时候,可不可以传参呢? 类似函数柯里化。

let foo = {
    value: 1
};

function getValue(name, age) {
    console.log(this.value);
    console.log(name, age);
}

let bindFoo = getValue.bind(foo, 'xql');
bindFoo('18');

② 另外,因为 bind 函数返回一个新函数,所以 bind 函数还有一个特点

当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数(调用 bind 方法的函数)当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

  var value = 2;
  
  var foo = {
      value: 1
  };

  function bar(name, age) {
      this.habit = 'shopping';
      console.log(this.value);
      console.log(name, age);
  }

  bar.prototype.friend = 'shuaige';

  let bindFoo = bar.bind(foo, 'xql');
  var obj = new bindFoo('18');
  // undefined 这里对 bind 函数绑定的 this 失效了
  // xql 18
  console.log(obj.habit);
  console.log(obj.friend);

注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,这里可以查看 new 的模拟实现,就会知道这个时候的 this 已经指向了 obj。

  1. 实现步骤
  • 首先实现传参;
Function.prototype.myBind = function (objCtx) {
    let ctx = objCtx || window;
    let _this = this;
    let args = [...arguments].slice(1);
    
    return function () {
        // 这里的 arguments 与上面的 arguments 的值是不同的 这里是对返回函数所传入的参数  下面是将类数组 arguments 转为数组的两种方式 这里在使用的时候要记得转化为数组 
        // args.concat(arguments) 这样使用会出现问题 arguments 是个类数组对象
        // let bindArgs = [...arguments]
        // let bindArgs = Array.prototype.slice.call(arguments)
        return _this.apply(ctx, args.concat(...arguments)) // 为什么要写return 因为绑定函数(函数)是可以有返回值的
    }
}

// 测试一下
  var foo = {
      value: 1
  };
  
  function bar(name, age) {
      this.habit = 'shopping';
      console.log(this.value);
      console.log(name, age);
  }
  
  let bindFoo = bar.myBind(foo, 'xql');
  bindFoo('1111')

  • 其次,实现绑定函数(调用 bind 方法的函数)作为构造函数进行使用的情况;
Function.prototype.myBind = function (objCtx) {
    let ctx = objCtx || window;
    let _this = this;
    let args = [...arguments].slice(1);
    
    let Fbind = function () {
        // 当 Fbind 作为构造函数时,这里的 this 指向实例
        let self = this instanceof Fbind ? this : ctx;
        return _this.apply(self, args.concat(...arguments));
    }
    
    Fbind.prototype = this.prototype; // // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    // 不要写成 Fbind.prototype = ctx.prototype;
    
    return Fbind;
}

这里,我们直接将 Fbind.prototype = this.prototype,会导致修改 Fbind.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转( js 高程中成为原型式继承)。

  1. 最终的实现为:
Function.prototype.myBind = function (objCtx) {
    if (typeof this !== 'function') {
        throw new TypeError('Error');
    }
    let ctx = objCtx || window;
    let _this = this;
    let args = [...arguments].slice(1);
    
    let Fbind = function () {
        let self = this instanceof Fbind ? this : ctx;
        return _this.apply(self, args.concat(...arguments));
        // 这里可以使用 call 方法
        // let bindArgs = args.concat(...arguments);
        // return _this.call(self, ...bindArgs);
    }
    
    let f = function () {};
    f.prototype = this.prototype;
    Fbind.prototype = new f();
    
    return Fbind;
}

 // 测试一下 
 var value = 2;

 var foo = {
     value: 1
 };

 function bar(name, age) {
     this.habit = 'shopping';
     console.log(this.value); // undefined
     console.log(name, age);
 }

 bar.prototype.friend = 'shuaige';

 var bindFoo = bar.myBind(foo, 'xql');
 var obj = new bindFoo('18');
 console.log(obj.habit);
 console.log(obj.friend);

其中,Object.create()模拟实现为:(Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 )

Object.create = function( o ) {
    function f(){}
    f.prototype = o;
    return new f;
};

4、new操作符的模拟实现

通过 new 操作符的模拟实现,来看看使用 new 获得构造函数实例的原理。

  1. 先来看看使用 new 操作符发生了什么?
  • 创建一个空对象;
  • 该空对象的原型指向构造函数(链接原型):将构造函数的 prototype 赋值给对象的 __proto__属性;
  • 绑定 this:将对象作为构造函数的 this 传进去,并执行该构造函数;
  • 返回新对象:如果构造函数返回的是一个对象,则返回该对象;否则(若没有返回值或者返回基本类型),返回第一步中新创建的对象;

举例说明:

  • 构造函数没有返回值的情况:
  function Angel (name, age) {
      this.name = name;
      this.age = age;
      this.habit = 'Games';
  }

  Angel.prototype.strength = 60;

  Angel.prototype.sayYourName = function () {
      console.log('I am ' + this.name);
  }

  var person = new Angel('xql', '18');
  console.log(person.name, person.habit, person.strength) // xql Games 60
  person.sayYourName(); // I am xql
  • 构造函数有返回值的情况:返回对象 or 返回基本类型(返回 null )
  function Angel (name, age) {
      this.strength = 60;
      this.age = age;

      return {
          name: name,
          habit: 'Games'
      }
  }

  var person = new Angel('xql', '18');
  console.log(person.name, person.habit) // xql Games 60
  console.log(person.strength, person.age) // undefined undefined
  function Angel (name, age) {
      this.strength = 60;
      this.age = age;

      return '111' // 和没有返回值以及 return null 效果是一样的
  }

  var person = new Angel('xql', '18');
  console.log(person.name, person.habit) // undefined undefined
  console.log(person.strength, person.age) // 60 "18"
  1. 具体实现

因为 new 是关键字, 我们只能写一个函数来模拟了,构造函数作为参数传入。

// 使用 new
var person = new Angel(……);
// 使用 objectFactory
var person = objFactory(Angel, ……)
function objFactory () {
  let obj = {};
  // let obj = new Object ();
  // 取出参数中的第一个参数即构造函数, 并将该参数从 arguments 中删掉
  let Con = [].shift.call(arguments); // 不要写成 [].prototype.shift.call(arguments)
  
  obj._proto_ = Con.prototype;
  
  let result = Con.apply(obj, arguments);
  // let result = Con.apply(obj, [...arguments]);
  // let result = Con.call(obj, ...arguments)
  
  return Object.prototype.toString.call(result) === '[object Object]' ? result : obj;
  // 这里用typeof result == 'object' 进行判断会有个问题:当构造函数返回 null 时,会报错,因为 typeof null == 'object'
  // 应该是除了构造函数返回一个对象,其他的都返回新创建的对象
}

 // 测试一下
function Angel (name, age) {
  this.strength = 60;
  this.age = age;
  return null
}

var person = objFactory(Angel, 'Kevin', '18');
console.log(person.name, person.habit); // undefined undefined
console.log(person.strength, person.age) // 60 "18"

3、 小知识点

  1. 对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。
  2. 对于 new 来说,还需要注意下运算符优先级。
function Foo() {
    return this;
}
Foo.getName = function () {
    console.log('1');
};
Foo.prototype.getName = function () {
    console.log('2');
};

new Foo.getName();   // -> 1
new Foo().getName(); // -> 2   

References

JavaScript深入系列
new

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容