js 手動實現bind方法,超詳細思路分析!

2021-03-02 字節愛好者

在模擬bind之前,我們先了解bind的概念,這裡引入MDN解釋:

bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其餘參數將作為新函數的參數,供調用時使用。

說的通俗一點,bind與apply/call一樣都能改變函數this指向,但bind並不會立即執行函數,而是返回一個綁定了this的新函數,你需要再次調用此函數才能達到最終執行。

我們來看一個簡單的例子:

var obj = {    z: 1};var obj1 = {    z: 2};
function fn(x, y) { console.log(x + y + this.z);};fn.call(obj, 2, 3); fn.apply(obj, [2, 3]);
var bound = fn.bind(obj, 2);bound(3); bound.call(obj1, 3);

可以到bind並不是立即執行,而是返回一個新函數,且新函數的this無法再次被修改,我們總結bind的特點:

可以修改函數this指向。

bind返回一個綁定了this的新函數boundFcuntion,例子中我們用bound表示。

支持函數柯裡化,我們在返回bound函數時已傳遞了部分參數2,在調用時bound補全了剩餘參數。

boundFunction的this無法再被修改,使用call、apply也不行。

考慮到有同學對於柯裡化的陌生,這裡簡單解釋,所謂函數柯裡化其實就是在函數調用時只傳遞一部分參數進行調用,函數會返回一個新函數去處理剩下的參數,一個經典簡單的例子:

function fn(x, y) {    return function (y) {        console.log(x + y);    };};var fn_ = fn(1);fn_(1); 
fn(1)(1)

不難發現函數柯裡化使用了閉包,在執行內層函數時,它使用了外層函數的局部形參x,從而構成了閉包,扯遠了點。

我們來嘗試實現bind方法,先從簡單的改變this和返回函數開始。

實現bind

之前已經有了模擬call/apply的經驗,這裡直接給出版本一:

Function.prototype.bind_ = function (obj) {    var fn = this;    return function () {        fn.apply(obj);    };};
var obj = { z: 1};
function fn() { console.log(this.z);};
var bound = fn.bind_(obj);bound();

唯一需要留意的就是var fn = this這一行,如果不提前保存,在執行bound時內部this會指向window。

版本一以滿足了this修改與函數返回,馬上有同學就想到了,版本一不支持函數傳參,那麼我們進行簡單修改讓其支持傳參:

Function.prototype.bind_ = function (obj) {        var args = Array.prototype.slice.call(arguments, 1);    var fn = this;    return function () {        fn.apply(obj, args);    };};

完美了嗎?並不完美,別忘了我們前面說bind支持函數柯裡化,在調用bind時可以先傳遞部分參數,在調用返回的bound時可以補全剩餘參數,所以還得進一步處理,來看看bind_第二版:

Function.prototype.bind_ = function (obj) {        var args = Array.prototype.slice.call(arguments, 1);    var fn = this;    return function () {                var params = Array.prototype.slice.call(arguments);                fn.apply(obj, args.concat(params));    };};
var obj = { z: 1};
function fn(x, y) { console.log(x + y + this.z);};
var bound = fn.bind_(obj, 1);bound(2);

看,改變this,返回函數,函數柯裡化均已實現。這段代碼需要注意的是args.concat(params)的順序,args在前,因為只有這樣才能讓先傳遞的參數和fn的形參按順序對應。

至少走到這一步都挺順序,需要注意的是,bind方法還有一個少見的特性,這裡引用MDN的描述

綁定函數也可以使用 new 運算符構造,它會表現為目標函數已經被構建完畢了似的。提供的 this 值會被忽略,但前置參數仍會提供給模擬函數。

說通俗點,通過bind返回的boundFunction函數也能通過new運算符構造,只是在構造過程中,boundFunction已經確定的this會被忽略,且返回的實例還是會繼承構造函數的構造器屬性與原型屬性,並且能正常接收參數。

有點繞口,我們來看個簡單的例子:

var z = 0;var obj = {    z: 1};
function fn(x, y) { this.name = '聽風是風'; console.log(this.z); console.log(x); console.log(y);};fn.prototype.age = 26;
var bound = fn.bind(obj, 2);var person = new bound(3);
console.log(person.name);

在此例子中,我們先是將函數fn的this指向了對象obj,從而得到了bound函數。緊接著使用new操作符構造了bound函數,得到了實例person。不難發現,除了先前綁定好的this丟失了(後面會解釋原因),構造器屬性this.name,以及原型屬性fn.prototype.age都有順利繼承,除此之外,兩個形參也成功傳遞進了函數。

難點來了,至少在ES6之前,JavaScript並沒有class類的概念,所謂構造函數其實只是對於類的模擬;而這就造成了一個問題,所有的構造函數除了可以使用new構造調用以外,它還能被普通調用,比如上面例子中的bound我們也可以普通調用:

有同學在這可能就有疑惑,bound()等同於window.bound(),此時this不是應該指向window從而輸出0嗎?我們在前面說bind屬於硬綁定,一次綁定終生受益,上面的調用本質上等同於:

函數fn存在this默認綁定window與顯示綁定bind,而顯示綁定優先級高於默認綁定,所以this還是指向obj。

當構造函數被new構造調用時,本質上構造函數中會創建一個實例對象,函數內部的this指向此實例,當執行到console.log(this.z)這一行時,this上並未被賦予屬性z,所以輸出undefined,這也解釋了為什麼bound函數被new構造時會丟失原本綁定的this。

是不是覺得ES5構造函數特別混亂,不同調用方式函數內部this指向還不同,也正因如此在ES6中隆重推出了class類,凡是通過class創建的類均只能使用new調用,普通調用一律報錯處理:

class Fn {    constructor(name, age) {        this.name = name;        this.age = age;    };    sayName() {        console.log(this.name);    };};const person = new Fn('聽風是風', 26);person.sayName(); const person1 = Fn(); 

扯遠了,讓我們回到上面的例子,說了這麼多無非是為了強調一點,我們在模擬bind方法時,返回的bound函數在調用時得考慮new調用與普通調用,畢竟兩者this指向不同。

再說直白一點,如果是new調用,bound函數中的this指向實例自身,而如果是普通調用this指向obj,怎麼區分呢?

不難,我們知道(強行讓你們知道)構造函數實例的constructor屬性永遠指向構造函數本身(這句話其實有歧義,具體我會在原型的文章中解釋),比如:

function Fn(){};var o = new Fn();console.log(o.constructor === Fn);

而構造函數在運行時,函數內部this指向實例,所以this的constructor也指向構造函數:

function Fn() {    console.log(this.constructor === Fn); };var o = new Fn();console.log(o.constructor === Fn); 

所以我就用constructor屬性來判斷當前bound方法調用方式,畢竟只要是new調用,this.constructor === Fn一定為true。

讓我們簡單改寫bind_方法,為bound方法新增this判斷以及原型繼承:

Function.prototype.bind_ = function (obj) {    var args = Array.prototype.slice.call(arguments, 1);    var fn = this;    var bound = function () {        var params = Array.prototype.slice.call(arguments);                fn.apply(this.constructor === fn ? this : obj, args.concat(params));    };        bound.prototype = fn.prototype;    return bound;};

有同學就問了,難道不應該是this.constructor===bound嗎?並不是,雖然new的是bound方法,本質上執行的還是fn,畢竟bound自身並沒有構造器屬性,這點關係還是需要理清。

其次還有個缺陷。雖然構造函數產生的實例都是獨立的存在,實例繼承而來的構造器屬性隨便你怎麼修改都不會影響構造函數本身:

function Fn() {    this.name = '聽風是風';    this.sayAge = function () {        console.log(this.age);    };};Fn.prototype.age = 26;
var o = new Fn();o.sayAge(); o.name = 'echo';var o1 = new Fn();console.log(o1.name)

但是如果我們直接修改實例原型,這就會對構造函數Fn產生影響,來看個例子:

function Fn() {    this.name = '聽風是風';    this.sayAge = function () {        console.log(this.age);    };};Fn.prototype.age = 26;
var o = new Fn();o.sayAge(); o.__proto__.age = 18;var o1 = new Fn();console.log(o1.age)

不難理解,構造器屬性(this.name,this.sayAge)在創建實例時,我們可以抽象的理解成實例深拷貝了一份,這是屬於實例自身的屬性,後面再改都與構造函數不相關。而實例要用prototype屬性時都是順著原型鏈往上找,構造函數有便借給實例用了,一共就這一份,誰要是改了那就都得變。

我們可以輸出實例o,觀察它的屬性,可以看到age屬性確實是綁原型__proto__上(注意,prototype是函數特有,普通對象只有__proto__,兩者指向相同)。

怎麼做才保險呢,這裡就可以藉助一個空白函數作為中介,直接看個例子:

function Fn() {    this.name = '聽風是風';    this.sayAge = function () {        console.log(this.age);    };};Fn.prototype.age = 26;var Fn1 = function () {};Fn1.prototype = Fn.prototype;var Fn2 = function () {};Fn2.prototype = new Fn1();var o = new Fn2();console.log(o.age); o.__proto__.age = 18;var o1 = new Fn();console.log(o1.age);

說到底,我們就是借用空白函數,讓Fn2的實例多了一層__proto__,達到修改原型不會影響Fn原型的目的,當然你如果通過__proto__.__proto__還是一樣能修改,差不多就是這個意思:

o.__proto__.__proto__.age = 18;var o1 = new Fn();console.log(o1.age);

所以綜上,我們再次修改bind_方法,拿出第四版:

Function.prototype.bind_ = function (obj) {    var args = Array.prototype.slice.call(arguments, 1);    var fn = this;        var fn_ = function () {};    var bound = function () {        var params = Array.prototype.slice.call(arguments);                fn.apply(this.constructor === fn ? this : obj, args.concat(params));        console.log(this);    };    fn_.prototype = fn.prototype;    bound.prototype = new fn_();    return bound;};

最後,bind方法如果被非函數調用時會拋出錯誤,所以我們要在第一次執行bind_時做一次調用判斷,加個條件判斷,我們來一個完整的最終版:

Function.prototype.bind_ = function (obj) {    if (typeof this !== "function") {        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");    };    var args = Array.prototype.slice.call(arguments, 1);    var fn = this;        var fn_ = function () {};    var bound = function () {        var params = Array.prototype.slice.call(arguments);                fn.apply(this.constructor === fn ? this : obj, args.concat(params));        console.log(this);    };    fn_.prototype = fn.prototype;    bound.prototype = new fn_();    return bound;};
var z = 0;var obj = { z: 1};
function fn(x, y) { this.name = '聽風是風'; console.log(this.z); console.log(x); console.log(y);};fn.prototype.age = 26;
var bound = fn.bind_(obj, 2);var person = new bound(3);
console.log(person.name); console.log(person.age); person.__proto__.age = 18;var person = new fn();console.log(person.age);

如果文章對你有幫助,歡迎關注+轉發,讓更多同學看到。

原文:https://www.cnblogs.com/echolun/p/12178655.html

相關焦點

  • 面試官問:能否模擬實現JS的bind方法(高頻考點)
    1.面試官問:能否模擬實現JS的new操作符2.面試官問:能否模擬實現JS的bind方法(本文)3.面試官問:能否模擬實現JS的call和apply方法4.面試官問:JS的this指向5.面試官問:JS的繼承用過React的同學都知道,經常會使用bind來綁定this。
  • JS中的call、bind使用場景解析-方法借用
    那麼這一篇就來一起分析一下,call和bind的使用場景。1.類數組對象借用數組方法        類數組對象就是有數字索引下標和length屬性,但是沒有數組對象具有的數組方法。比如函數的arguments對象,DOM方法或者JQuery方法的返回的結果。
  • 超詳細 ElementUI 源碼分析 —— Select(模板篇)
    在 ElementUI 官方文檔上有詳細使用功能及方法,不熟悉的同學可以先研究一下。這個 el-input 渲染出來就是 ElementUI 封裝的 input 組件,如果你沒有看過,可以先移步超詳細 ElementUI 源碼分析系列仔細閱讀一下 input 源碼。
  • 超詳細的 JS 數組方法
    https://juejin.cn/post/6907109642917117965數組是 js 中最常用到的數據集合,其內置的方法有很多,熟練掌握這些方法,可以有效的提高我們的工作效率,同時對我們的代碼質量也是有很大影響。
  • JavaScript深入之bind的模擬實現
    JavaScript深入之bind的模擬實現JavaScript深入系列第十一篇,通過bind函數的模擬實現,帶大家真正了解bind的特性bind一句話介紹 bind:bind() 方法會創建一個新函數。當這個新函數被調用時,bind() 的第一個參數將作為它運行時的 this,之後的一序列參數將會在傳遞的實參前傳入作為它的參數。
  • Vue.js最佳實踐(五招讓你成為Vue.js大師)
    如果有人需要Vue.js入門系列的文章可以在評論區告訴我,有空就給你們寫。對大部分人來說,掌握Vue.js基本的幾個API後就已經能夠正常地開發前端網站。但如果你想更加高效地使用Vue來開發,成為Vue.js大師,那下面我要傳授的這五招你一定得認真學習一下了。
  • 5招讓你成為Vue.js大師
    如果有人需要Vue.js入門系列的文章可以在評論區告訴我,有空就給你們寫。對大部分人來說,掌握Vue.js基本的幾個API後就已經能夠正常地開發前端網站。但如果你想更加高效地使用Vue來開發,成為Vue.js大師,那下面我要傳授的這五招你一定得認真學習一下了。
  • 實現js的replaceAll方法
    實現js的replaceAll方法 發表於2009-12-08 13:21| 來源CSDN博客| 作者herrapfel(趙根)
  • 22個超詳細的 JS 數組方法
    https://juejin.cn/post/6907109642917117965數組是 js 中最常用到的數據集合,其內置的方法有很多,熟練掌握這些方法,可以有效的提高我們的工作效率,同時對我們的代碼質量也是有很大影響。
  • 瀏覽器中實現深度學習?有人分析了7個基於JS語言的DL框架
    機器之心分析師網絡作者:仵冀穎編輯:H4O本文中,作者基於WWW』19 論文提供的線索,詳細解讀了在瀏覽器中實現深度學習的可能性、可行性和性能現狀。[1]作為參考主線,具體分析和探討在瀏覽器中實現深度學習的問題。1、瀏覽器中支持的深度學習功能在這一章節中,我們以文獻 [1] 為參考主線,重點探討現有的框架提供了哪些特性來支持在瀏覽器中實現各種 DL 任務。
  • ReactJS,AngularJS, Vue.js優劣對比分析
    Vue.js框架一直處於墊底狀態,不是很流行。不過呢,也有逐漸轉熱的趨勢,我相信它會越來越熱門。總的來說,React和Angular一直保持著相對一致的發展步調。如果要我嘗試做個預測,那麼React會持續升高,Angular有所下滑。Vue.js依然不是很明晰,不過由於其框架的簡潔性,發展也不錯。我們同時也分析了世界範圍內前端招聘對框架要求的數據。
  • Vue.js 教程:構建一個特斯拉汽車餘電計算器
    cdworkshop-reactjs-vuejs/vuejs-app閱讀 README.md,了解我們要執行的任務。上圖是我們將要構建的應用程式的示例。我們先從一個有問題的應用程式開始入手,需要修復它的問題並做進一步的開發。在開始之前,首先解釋一下這個應用程式的結構。
  • 瀏覽器中實現深度學習?有人分析了7個基於JS語言的DL框架,發現還有...
    [1]作為參考主線,具體分析和探討在瀏覽器中實現深度學習的問題。1、瀏覽器中支持的深度學習功能在這一章節中,我們以文獻 [1] 為參考主線,重點探討現有的框架提供了哪些特性來支持在瀏覽器中實現各種 DL 任務。我們首先介紹了進行分析的幾個框架,然後從兩個方面比較了這些框架的特性:提供的功能和開發人員的支持。
  • 前端:js中修改this的指向方法整理
    call 方法可以在一個對象上借用另一個對象的方法案例:Object.prototype.tostring.call([])。這是call的核心功能,它允許你在一個對象上調用該對象沒有定義的方法,並且這個方法可以訪問該對象中的屬性還可以通過call方法來調用匿名函數在下例中的for循環體內,我們創建了一個匿名函數,然後通過調用該函數的call方法,將每個數組元素作為指定的this
  • Socket網絡編程核心API深入分析(一):bind函數
    注意:本片文章涉及到的內核源碼來自linux內核版本3.6簡單的伺服器與客戶端實現本篇文章的重點在於從底層深入分析bind()函數,相信已經能夠自己實現一個簡單的伺服器和客戶端並進行交互,下面是一個簡單的demo,幫助大家複習一下socket編程api的調用過程。
  • nodejs v14源碼分析之event模塊
    events模塊是Node.js中比較簡單但是卻非常核心的模塊,Node.js中,很多模塊都繼承於events模塊,events模塊是發布、訂閱模式的實現。我們首先看一下如何使用events模塊。1. const { EventEmitter } = require('events'); 2.
  • Vue.js深入學習
    vue.jsv-cloak:解決網速慢閃爍問題 ,不會替換掉標籤裡面的內容v-text:會替換掉標籤裡面的內容:原樣輸出v-html:會解析標籤v-bind:綁定屬性的指令,縮寫: 只能實現數據的單向綁定。
  • 超詳細的 JS 數組方法整理出來了
    https://juejin.cn/post/6907109642917117965數組是 js 中最常用到的數據集合,其內置的方法有很多,熟練掌握這些方法,可以有效的提高我們的工作效率,同時對我們的代碼質量也是有很大影響。
  • Backbone.js教程 第二章 Backbonejs中的Model實踐
    上一章主要是通過簡單的代碼對Backbonejs做了一個概括的展示,這一章開始從Model層說起,詳細解釋Backbonejs中的Model這個東西
  • 帝國cms+jquery.lazyload.js實現圖片延遲懶加載的極簡方法
    對於沒有專業程式設計師參與的網站運營管理者來說,要實現懶加載並不是那麼容易。網上有一些教程,厲害的高手是自己寫原生js來解決,更多快捷解決方案是使用jquery.lazyload.js插件,有示例代碼及演示(請自行搜索或參見此處連結),這個插件的使用並不難,如果是從零開始構建一個頁面,直接套用是沒有問題的,可是要套用在帝國cms製作好的網站裡,得用什麼方式方法呢?