JavaScript深入理解系列:bind方法詳解以及手寫

来源:https://www.cnblogs.com/GoodPingGe/archive/2022/04/14/16145269.html
-Advertisement-
Play Games

定義 bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其餘參數將作為新函數的參數,供調用時使用。 由定義可知,函數調用bind()方法的時候,會返回一個新函數,並且this指向bind函數的第一個參數,簡單來表示。 fn ...


定義

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

由定義可知,函數調用bind()方法的時候,會返回一個新函數,並且this指向bind函數的第一個參數,簡單來表示。

fn.bind(obj對象)執行 返回一個函數,想調用的時候,fn.bind(obj)()這樣來執行;

舉個例子:

var name = '炒米粉';
var obj = {
	name: '程式員米粉'
};
function fn() {
	console.log(this.name);
}

var getFn = fn.bind(obj); // 返回一個函數命名getFn
getFn(); // this.name => '程式員米粉', this指向obj
fn(); // this.name => '炒米粉', this指向window

總結:

1、bind()執行,返回一個函數。
2、bind()接受多個參數。

第1步:模擬返回一個函數

由上述定義以及總結可知,我們首先模擬一個函數調用bind()方法返回一個函數:

Function.prototype.myBind = function(context) {
	var context = context || window;
	var self = this;
	return function() {
		self.apply(context);
	};
};

// 使用一下
var getFn = fn.myBind(obj); // 返回一個函數命名getFn
getFn(); // this.name => '程式員米粉', this指向obj

apply實現可以看我以往寫的 《javascript深入系列之手寫call與apply》

那我們把fn再改造一下,加一個返回值

function fn() {
	console.log(this.name);
    return 1
}
// 使用一下
var getFn = fn.myBind(obj); // 返回一個函數命名getFn
console.log(getFn()); // 發現並沒有把目標值1返回回來

上面的例子getFn()執行發現並沒有把目標值1返回回來,那我們需要myBind函數裡面添加 return self.apply(context);

最終代碼:

Function.prototype.myBind = function(context) {
	var context = context || window;
	var self = this;
	return function() {
        // 這裡加了return
		return self.apply(context);
	};
};
// 使用一下
var getFn = fn.myBind(obj); // 返回一個函數命名getFn
console.log(getFn()); // 1, 目標值達到

第2步:模擬返回接受多個參數

我們先來看看bind是怎樣接收多個參數的。還是原來的例子:

var name = '炒米粉';
var obj = {
	name: '程式員米粉'
};
function fn(age, weight) {
	console.log(this.name);
    console.log(age, weight);
}

// var getFn = fn.bind(obj, 18, 150); 
// getFn(); // this.name => '程式員米粉',age, weight => 18, 150

// var getFn = fn.bind(obj); 
// getFn( 18, 150); // this.name => '程式員米粉',age, weight => 18, 150

var getFn = fn.bind(obj, 18); 
getFn(150); // this.name => '程式員米粉',age, weight => 18, 150

從例子可以看出來,參數可以在兩個地方傳進來。在用bind綁定函數fn的時候傳參數18, fn.bind(obj, 18); 返回函數的時候傳參數15 getFn(150)

思考一下,我們如何把這兩個地方傳進來的參數合併一下。別問,我們可以用arguments對象獲取傳傳進來的參數合併一下。那我們繼續修改一下代碼:

Function.prototype.myBind = function(context) {
	var context = context || window;
	var self = this;
     // myBind函數傳進來的參數,從第2個開始取 
    var args = arguments.length && Array.prototype.slice.call(1, arguments.length) || [];
     // array.slice(start, end)
	return function() {
        // 返回函數傳進來的參數,從第1個開始取 
        var arr = arguments.length && Array.prototype.slice.call(0, arguments.length) || [];
		return self.apply(context, arr.concat(args));
	};
};

var getFn = fn.bind(obj, 18); 
getFn(150); // age, weight => 18, 150

好了我們已經搞定了總結裡面的兩點了。

第3步:模擬構造函數效果

bind()方法還有一種特殊的用法,那就是原函數使用bind()方法綁定函數之後,原函數可以作為構造函數使用的。

綁定函數自動適應於使用 new 操作符去構造一個由目標函數創建的新實例。當一個綁定函數是用來構建一個值的,原來提供的 this 就會被忽略。 --來源moz

這什麼意思呢?那我們先來代碼看一下:

var name = '炒米粉';
var obj = {
	name: '程式員米粉'
};
function fn(age, weight) {
	console.log(this.name);
	console.log(age, weight);
    this.phoneNumber = '188888888'
}

fn.prototype.haveMoney = 'NO';
var getFn = fn.bind(obj, 18);
// 通過使用bind()方法返回的綁定函數,可以使用new創建實例對象。fn的this不再指向obj,而是新創建的對象newObj
var newObj = new getFn(150); 
// fn函數
// this.name => undefined;
// age, weight => 18, 50
newObj.haveMoney // => 'NO'
newObj.phoneNumber // => '188888888'

由上面的例子可以看出來this.name變成了undefined,為什麼呢?由於使用了new創建了新對象newObjthis.name中的this失效了,this指向了newObj,如果沒有用new創建新對象,this是指向bind(obj)中的obj的。原函數fn就作為了一個構造函數使用,所以newObj作為new創建出來的實例,可以繼承fn的原型上的屬性和方法,可見 newObj.haveMoney // => 'NO',newObj.phoneNumber // => '188888888'

繼續改進一下我們myBind方法:

var name = '炒米粉';
var obj = {
	name: '程式員米粉'
};
function fn(age, weight) {
	console.log(this.name);
	console.log(age, weight);
	this.phoneNumber = '188888888';
}

fn.prototype.haveMoney = 'NO';

Function.prototype.myBind = function(context) {
	var context = context || window;
	var self = this;
	// myBind函數傳進來的參數,從第2個開始取
	var args = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];
	var returnFn = function() {
		var arr = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];
        // this instanceof returnFn 什麼意思?這是一個判斷對象是否屬於繼承於當前對象
        // 如果為true,那就是返回的函數returnFn作為了一個構造函數使用,那麼this指向新實例,不再是context
        // 如果為false,那就是返回的函數returnFn作為了一個普通函數使用,那麼this指向context或者window

		return self.apply(this instanceof returnFn ? this : context, arr.concat(args));
	};
    // 這樣寫,使returnFn作為一個構造函數使用的時候,那麼通過原型繼承的方法,returnFn可以繼承當前對象裡面原型上面所有屬性和方法
    // 修正prototype之後,創建的新實例,可以繼承原型的屬性和方法,例如例子,實例newObj繼承了,this.phoneNumber中的屬性值。
    returnFn.prototype = this.prototype;
	return returnFn;
};

var getFn = fn.myBind(obj, 18);
var newObj = new getFn(150);
// console.log(this.name); => undefined;
// console.log(age, weight); => 18, 50
newObj.haveMoney // => 'NO'
newObj.phoneNumber // => '188888888'

上面例子比較難懂就是this instanceof returnFn ? this : contextreturnFn.prototype = this.prototype;,這都是跟js裡面的原型繼承相關知識點。

第4步:最終版本

好了,我們已經完成了90%了,但是還有點小瑕疵,關於原型繼承的,我們先來看看例子:

function fn() {}
Function.prototype.myBind = function(context) {
	var context = context || window;
	var self = this;
	// myBind函數傳進來的參數,從第2個開始取
	var args = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];

	var returnFn = function() {
		var arr = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];
        // this instanceof returnFn 什麼意思?這是一個判斷對象是否屬於繼承於當前對象
        // 如果  為true,那就是返回的函數returnFn作為了一個構造函數使用,那麼this指向新實例,不再是context
        // 如果 為false,那就是返回的函數returnFn作為了一個普通函數使用,那麼this指向context或者window

		return self.apply(this instanceof returnFn ? this : context, arr.concat(args));
	};
    // 這樣寫,使returnFn作為一個構造函數使用的時候,那麼通過原型繼承的方法,returnFn可以繼承當前對象裡面原型上面所有屬性和方法
    // 修正prototype之後,創建的新實例,可以繼承原型的屬性和方法,例如例子,實例newObj繼承了,this.phoneNumber中的屬性值。
    returnFn.prototype = this.prototype;
	return returnFn;
};
var getFn = fn.myBind(obj);
getFn.prototype.text = '我是被bind方法返回的函數'
console.log(fn.prototype.text) // '我是被bind方法返回的函數',理論上這裡不應該被修改,應該為undefined

由上述例子可以看出來,我們只是修改了通過用了bind方法返回函數的prototype對象添加了屬性,那麼原函數的原型對象prototype也被修改了。這樣是不行的。所以我們繼續優化了方法。

function fn() {}
Function.prototype.myBind = function(context) {
	if (typeof this !== 'function') {
		throw new Error('The bind method is not available');
	}
	var context = context || window;
	var self = this;
	// myBind函數傳進來的參數,從第2個開始取
	var args = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];

	var returnFn = function() {
		var arr = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];
		return self.apply(this instanceof returnFn ? this : context, arr.concat(args));
	};

    var templateFn = function() {};
    // 修正prototype之後,templateFn可以繼承原函數原型的屬性和方法
    templateFn.prototype = this.prototype;
    // 創建空函數實例,然後給returnFn的原型繼承,returnFn創建的實例可以繼承templateFn方法。
    // 這樣修改了returnFn原型方法,都影響不了 原函數myBind原型對象,只會影響空函數templateFn。妙妙妙啊。
    returnFn.prototype = new templateFn();
	return returnFn;
};

var getFn = fn.myBind(obj);
var newObj = new getFn();
getFn.prototype.text = '我是被bind方法返回的函數'
console.log(fn.prototype.text) // => undefined

上述例子,可以看出來我們通過一個空函數 var templateFn = function() {};,那麼空函數templateFn的原型繼承原函數myBind的原型對象,再把返回函數returnFn繼承空函數templateFn的實例屬性和方法。這樣子改變返回函數templateFn的原型對象,不再影響原函數了myBind原型對象,簡直一個妙字。

防止調用不是函數情況

if (typeof this !== 'function') {
		throw new Error('The bind method is not available');
	}

題外話:apply、call、bind三者的區別

  • 三者都可以改變函數的this對象指向。

  • 三者第一個參數都是this要指向的對象,如果沒有這個參數或參數為undefined或null,則預設指向全局window。

  • 三者都可以傳參,但是apply是數組,而call是參數列表,且apply和call是一次性傳入參數,而bind可以分為多次傳入,bind是返回綁定this之後的函數,apply、call 則是立即執行。

結語

希望看完這篇文章對你有幫助:

  • 理解bind原理
  • 實踐寫一個bind方法
  • 區分apply、call、bind三者的區別

文中如有錯誤,歡迎在評論區指正,如果這篇文章幫助到了你,歡迎點贊和關註,後續會輸出更好的分享。

歡迎關註公眾號:【程式員米粉】
公眾號分享開發編程、職場晉升、大廠面試經驗

本文來自博客園,作者:程式員米粉,轉載請註明原文鏈接:https://www.cnblogs.com/GoodPingGe/p/16145269.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文為 SQL 初學者介紹了 SQL 究竟是什麼,以及它能做什麼事情。因為 SQL 是用來與資料庫打交道的,所以,我們也介紹了一些基本的資料庫術語。 一、資料庫基礎 你正在讀這這一篇文章,這表明你需要以某種方式與資料庫打交道。SQL 正是用來實現這一任務的語言,因此在學習 SQL 之前,你應該對數據 ...
  • 本章目錄 0x00 數據持久化 1.RDB 方式 2.AOF 方式 如何抉擇 RDB OR AOF? 0x01 備份容災 一、備份 1.手動備份redis資料庫 2.遷移Redis指定db-資料庫 3.Redis集群數據備份與遷移 二、恢復 1.系統Redis用戶被刪除後配置數據恢復流程 2.Kub ...
  • 一、Kotlin基礎 1.數據類型聲明 在Kotlin中要定義一個變數需要使用var關鍵字 //定義了一個可以修改的Int類型變數 var number = 39 如果要定義一個常量可以使用val關鍵字,等價於Java的final關鍵字. val name = "miku" //給val定義的常量再 ...
  • 在HarmonyOS開發中List下拉刷新是一種很常見的問題,今天描述怎麼實現List下拉刷新的功能實現,主要分為“開發準備”,“代碼實現”,“運行效果” 1. 開發準備 我們需要學習以下知識點 1.1 【Harmony OS】【ARK UI】【Demo】載入動畫實現 1.2 PanGesture ...
  • 華為開發者聯盟與艾瑞咨詢聯合發佈《2022年移動應用趨勢洞察白皮書》,本白皮書主要分析移動應用行業發展現狀和趨勢,並對影音娛樂、通訊社交、電商生活、運動健康、出行導航等細分行業場景進行分析,把握移動應用細分行業發展特色和趨勢,為廣大開發者的開發和運營決策提供參考。 華為開發者聯盟一直致力於全方位聯接 ...
  • 初識前後端 在學習瞭解前後端的過程中,自己看到了這一篇好的文章,摘下了一些當下用的的內容,供複習參考。 什麼是前端開發? 前端開發主要涉及網站和 App,用戶能夠從 App 屏幕或瀏覽器上看到東西。簡單地說,能夠從 App 屏幕和瀏覽器上看到的東西都屬於前端。 網站和移動 App 的前端 **對於網 ...
  • 這幾天天天搞到這麼晚,我看今天的內容看起不多啊,不知道為什麼學著學著就到了這麼晚。今天的內容還是有點多哈,有點自我矛盾了,再次一一道來。 1. 首先今天先看到路由的概念,什麼叫做路由? 路由就是映射關係,在express中路由指的是客戶端請求和伺服器處理函數的映射關係,路由有三部分組成:請求類型、請 ...
  • 一、背景由來 cookie原來是用來網路請求攜帶用戶信息的,只不過在HTML5出現之前,前端沒有本地存儲的方法,只能使用cookie代替 localstorge、sessionStorge是html5提供的API,極大的方便了前端在客戶端存儲數據 二、那麼這三者有什麼區別呢? 1.存儲時間 cook ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...