模擬call、apply和bind方法以及new操作符的實(shí)現(xiàn)

在模擬實(shí)現(xiàn) call、apply 和 bind 方法以及 new 操作符的過(guò)程中,能讓我們更好的理解他們的運(yùn)行原理。

1、區(qū)別

call 、apply 和 bind 方法都是為了解決改變 this 的指向。

call 、apply 方法都是在傳入一個(gè)指定的 this 值和若干個(gè)指定的參數(shù)值的前提下調(diào)用某個(gè)函數(shù)或方法。作用都是相同的,只是傳參的形式不同。
除了第一個(gè)參數(shù)外,call 接收一個(gè)參數(shù)列表,apply 接受一個(gè)參數(shù)數(shù)組。使用 call 、apply 方法該函數(shù)會(huì)被執(zhí)行

bind 和其他兩個(gè)方法作用也是一致的,只是該方法會(huì)返回一個(gè)函數(shù)。即使用 bind 方法該函數(shù)不會(huì)被執(zhí)行(返回該函數(shù))。并且我們可以通過(guò) bind 實(shí)現(xiàn)柯里化。

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')() 執(zhí)行該函數(shù)

2、模擬實(shí)現(xiàn) call 和 apply

  1. 先看看call 和 apply 方法發(fā)生了什么?
  • 改變了 this 的指向,指向到 a
  • getValue 函數(shù)執(zhí)行了
  1. 該怎么模擬實(shí)現(xiàn)這兩個(gè)效果呢?

既然 call 和 apply 方法是改變了 this 指向,讓新的對(duì)象可以執(zhí)行該函數(shù),那么思路是否可以變成給新的對(duì)象添加一個(gè)函數(shù)(將函數(shù)設(shè)為對(duì)象的屬性),然后在執(zhí)行完以后刪除?

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; // 函數(shù)是可以有返回值的
}

// 測(cè)試一下
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 是對(duì)象的屬性名,因?yàn)樽詈笠猟elete,這里可以是任意名稱。

以上就是 call 方法的思路,apply 方法的實(shí)現(xiàn)也類似。

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

    let ctx = objCtx || window;
    ctx.fn = this; // 屬性名fn可以任意
    
    let result;
    // 需要判斷是否存在第二個(gè)參數(shù)
    if (arguments[1]) {
        result = ctx.fn(...arguments[1]);
    } else {
        result = ctx.fn();
    }
    delete ctx.fn;
    return result;
}

3、bind 方法的模擬實(shí)現(xiàn)

  1. bind 函數(shù)的兩個(gè)特點(diǎn)?
  • 可以傳入?yún)?shù)
  • 返回一個(gè)函數(shù)

① 其中,對(duì)于 bind 函數(shù)中的傳入?yún)?shù)有個(gè)特點(diǎn)

既然在使用 bind 函數(shù)的時(shí)候,是可以傳參的。那么,在執(zhí)行 bind 返回的函數(shù)的時(shí)候,可不可以傳參呢? 類似函數(shù)柯里化。

let foo = {
    value: 1
};

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

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

② 另外,因?yàn)?bind 函數(shù)返回一個(gè)新函數(shù),所以 bind 函數(shù)還有一個(gè)特點(diǎn)

當(dāng) bind 返回的函數(shù)作為構(gòu)造函數(shù)的時(shí)候,bind 時(shí)指定的 this 值會(huì)失效,但傳入的參數(shù)依然生效。

一個(gè)綁定函數(shù)也能使用new操作符創(chuàng)建對(duì)象:這種行為就像把原函數(shù)(調(diào)用 bind 方法的函數(shù))當(dāng)成構(gòu)造器。提供的 this 值被忽略,同時(shí)調(diào)用時(shí)的參數(shù)被提供給模擬函數(shù)。

  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 這里對(duì) bind 函數(shù)綁定的 this 失效了
  // xql 18
  console.log(obj.habit);
  console.log(obj.friend);

注意:盡管在全局和 foo 中都聲明了 value 值,最后依然返回了 undefind,說(shuō)明綁定的 this 失效了,這里可以查看 new 的模擬實(shí)現(xiàn),就會(huì)知道這個(gè)時(shí)候的 this 已經(jīng)指向了 obj。

  1. 實(shí)現(xiàn)步驟
  • 首先實(shí)現(xiàn)傳參;
Function.prototype.myBind = function (objCtx) {
    let ctx = objCtx || window;
    let _this = this;
    let args = [...arguments].slice(1);
    
    return function () {
        // 這里的 arguments 與上面的 arguments 的值是不同的 這里是對(duì)返回函數(shù)所傳入的參數(shù)  下面是將類數(shù)組 arguments 轉(zhuǎn)為數(shù)組的兩種方式 這里在使用的時(shí)候要記得轉(zhuǎn)化為數(shù)組 
        // args.concat(arguments) 這樣使用會(huì)出現(xiàn)問(wèn)題 arguments 是個(gè)類數(shù)組對(duì)象
        // let bindArgs = [...arguments]
        // let bindArgs = Array.prototype.slice.call(arguments)
        return _this.apply(ctx, args.concat(...arguments)) // 為什么要寫return 因?yàn)榻壎ê瘮?shù)(函數(shù))是可以有返回值的
    }
}

// 測(cè)試一下
  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')

  • 其次,實(shí)現(xiàn)綁定函數(shù)(調(diào)用 bind 方法的函數(shù))作為構(gòu)造函數(shù)進(jìn)行使用的情況;
Function.prototype.myBind = function (objCtx) {
    let ctx = objCtx || window;
    let _this = this;
    let args = [...arguments].slice(1);
    
    let Fbind = function () {
        // 當(dāng) Fbind 作為構(gòu)造函數(shù)時(shí),這里的 this 指向?qū)嵗?        let self = this instanceof Fbind ? this : ctx;
        return _this.apply(self, args.concat(...arguments));
    }
    
    Fbind.prototype = this.prototype; // // 修改返回函數(shù)的 prototype 為綁定函數(shù)的 prototype,實(shí)例就可以繼承綁定函數(shù)的原型中的值
    // 不要寫成 Fbind.prototype = ctx.prototype;
    
    return Fbind;
}

這里,我們直接將 Fbind.prototype = this.prototype,會(huì)導(dǎo)致修改 Fbind.prototype 的時(shí)候,也會(huì)直接修改綁定函數(shù)的 prototype。這個(gè)時(shí)候,我們可以通過(guò)一個(gè)空函數(shù)來(lái)進(jìn)行中轉(zhuǎn)( js 高程中成為原型式繼承)。

  1. 最終的實(shí)現(xiàn)為:
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;
}

 // 測(cè)試一下 
 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()模擬實(shí)現(xiàn)為:(Object.create()方法創(chuàng)建一個(gè)新對(duì)象,使用現(xiàn)有的對(duì)象來(lái)提供新創(chuàng)建的對(duì)象的__proto__。 )

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

4、new操作符的模擬實(shí)現(xiàn)

通過(guò) new 操作符的模擬實(shí)現(xiàn),來(lái)看看使用 new 獲得構(gòu)造函數(shù)實(shí)例的原理。

  1. 先來(lái)看看使用 new 操作符發(fā)生了什么?
  • 創(chuàng)建一個(gè)空對(duì)象;
  • 該空對(duì)象的原型指向構(gòu)造函數(shù)(鏈接原型):將構(gòu)造函數(shù)的 prototype 賦值給對(duì)象的 __proto__屬性;
  • 綁定 this:將對(duì)象作為構(gòu)造函數(shù)的 this 傳進(jìn)去,并執(zhí)行該構(gòu)造函數(shù);
  • 返回新對(duì)象:如果構(gòu)造函數(shù)返回的是一個(gè)對(duì)象,則返回該對(duì)象;否則(若沒(méi)有返回值或者返回基本類型),返回第一步中新創(chuàng)建的對(duì)象;

舉例說(shuō)明:

  • 構(gòu)造函數(shù)沒(méi)有返回值的情況:
  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
  • 構(gòu)造函數(shù)有返回值的情況:返回對(duì)象 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' // 和沒(méi)有返回值以及 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. 具體實(shí)現(xiàn)

因?yàn)?new 是關(guān)鍵字, 我們只能寫一個(gè)函數(shù)來(lái)模擬了,構(gòu)造函數(shù)作為參數(shù)傳入。

// 使用 new
var person = new Angel(……);
// 使用 objectFactory
var person = objFactory(Angel, ……)
function objFactory () {
  let obj = {};
  // let obj = new Object ();
  // 取出參數(shù)中的第一個(gè)參數(shù)即構(gòu)造函數(shù), 并將該參數(shù)從 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' 進(jìn)行判斷會(huì)有個(gè)問(wèn)題:當(dāng)構(gòu)造函數(shù)返回 null 時(shí),會(huì)報(bào)錯(cuò),因?yàn)?typeof null == 'object'
  // 應(yīng)該是除了構(gòu)造函數(shù)返回一個(gè)對(duì)象,其他的都返回新創(chuàng)建的對(duì)象
}

 // 測(cè)試一下
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、 小知識(shí)點(diǎn)

  1. 對(duì)于創(chuàng)建一個(gè)對(duì)象來(lái)說(shuō),更推薦使用字面量的方式創(chuàng)建對(duì)象(無(wú)論性能上還是可讀性)。因?yàn)槟闶褂?new Object() 的方式創(chuàng)建對(duì)象需要通過(guò)作用域鏈一層層找到 Object,但是你使用字面量的方式就沒(méi)這個(gè)問(wèn)題。
  2. 對(duì)于 new 來(lái)說(shuō),還需要注意下運(yùn)算符優(yōu)先級(jí)。
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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,690評(píng)論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,019評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,188評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,718評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,438評(píng)論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,667評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,845評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,384評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容