摘要:理解文章中已經比較全面的分析了在中的指向問題,用一句話來總結就是的指向一定是在執行時決定的,指向被調用函數的對象。與和直接執行原函數不同的是,返回的是一個新函數。這個新函數包裹了原函數,并且綁定了的指向為傳入的。
理解 JavaScript this 文章中已經比較全面的分析了 this 在 JavaScript 中的指向問題,用一句話來總結就是:this 的指向一定是在執行時決定的,指向被調用函數的對象。當然,上篇文章也指出可以通過 call() / apply() / bind() 這些內置的函數方法來指定 this 的指向,以達到開發者的預期,而這篇文章將進一步來討論這個問題。
先來回顧一下,舉個簡單的例子:
var leo = { name: "Leo", sayHi: function() { return "Hi! I"m " + this.name; } }; var neil = { name: "Neil" }; leo.sayHi(); // "Hi! I"m Leo" leo.sayHi.call(neil); // "Hi! I"m Neil"基本用法
在 JavaScript 中,函數也是對象,所以 JS 的函數有一些內置的方法,就包括 call(), apply() 和 bind(),它們都定義在 Function 的原型上,所以每一個函數都可以調用這 3 個方法。
Function.prototype.call(thisArg [, arg1 [, arg2, ...]]),對于 call() 而言,它的第一個參數為需要綁定的對象,也就是 this 指向的對象,比如今天的引例中就是這樣。
第一個參數也可以是 null 和 undefined,在嚴格模式下 this 將指向瀏覽器中的 window 對象或者是 Node.js 中的 global 對象。
var leo = { name: "Leo", sayHi: function() { return "Hi! I"m " + this.name; } }; leo.sayHi.call(null); // "Hi! I"m undefined"
▲ this 指向 window,window.name 沒有定義
除了第一個參數,call() 還可以選擇接收剩下任意多的參數,這些參數都將作為調用函數的參數,來看一下:
function add(a, b) { return a + b; } add.call(null, 2, 3); // 5
▲ 等同于 add(2, 3)
apply() 的用法和 call() 類似,唯一的區別是它們接收參數的形式不同。除了第一個參數外,call() 是以枚舉的形式傳入一個個的參數,而 apply() 是傳入一個數組。
function add(a, b) { return a + b; } add.apply(null, [2, 3]); // 5
注意:apply() 接受的第二個參數為數組(也可以是一個類數組對象),但不意味著調用它的函數接收的是數組參數。這里的 add() 函數依舊是 a 和 b 兩個參數,分別賦值為 2 和 3,而不是 a 被賦值為 [2, 3]。
接下來說說 bind(),它和另外兩個大有區別。
var leo = { name: "Leo", sayHi: function() { return "Hi! I"m " + this.name; } }; var neil = { name: "Neil" }; var neilSayHi = leo.sayHi.bind(neil); console.log(typeof neilSayHi); // "function" neilSayHi(); // "Hi! I"m Neil"
與 call() 和 apply() 直接執行原函數不同的是,bind() 返回的是一個新函數。簡單說,bind() 的作用就是將原函數的 this 綁定到指定對象,并返回一個新的函數,以延遲原函數的執行,這在異步流程中(比如回調函數,事件處理程序)具有很強大的作用。你可以將 bind() 的過程簡單的理解為:
function bind(fn, ctx) { return function() { fn.apply(ctx, arguments); }; }如何實現
這一部分應該是經常出現在面試中。最常見的應該是 bind() 的實現,就先來說說如何實現自己的 bind()。
◆ bind() 的實現上一節已經簡單地實現了一個 bind(),稍作改變,為了和內置的 bind() 區別,我么自己實現的函數叫做 bound(),先看一下:
Function.prototype.bound = function(ctx) { var fn = this; return function() { return fn.apply(ctx); }; }
這里的 bound() 模擬了一個最基本的 bind() 函數的實現,即返回一個新函數。這個新函數包裹了原函數,并且綁定了 this 的指向為傳入的 ctx。
對于內置的 bind() 來說,它還有一個特點:
var student = { id: "2015" }; function showDetail (name, major) { console.log("The id " + this.id + " is for " + name + ", who major in " + major); } showDetail.bind(student, "Leo")("CS"); // "The id 2015 is for Leo, who major in CS" showDetail.bind(student, "Leo", "CS")(); // "The id 2015 is for Leo, who major in CS"
在這里兩次調用參數傳遞的方式不同,但是具有同樣的結果。下面,就繼續完善我們自己的 bound() 函數。
var slice = Array.prototype.slice; Function.prototype.bound = function(ctx) { var fn = this; var _args = slice.call(arguments, 1); return function() { var args = _args.concat(slice.call(arguments)); return fn.apply(ctx, args); }; }
這里需要借助 Array.prototype.slice() 方法,它可以將 arguments 類數組對象轉為數組。我們用一個變量保存傳入 bound() 的除第一個參數以外的參數,在返回的新函數中,將傳入新函數的參數與 bound() 中的參數合并。
其實,到現在整個 bound() 函數的實現都離不開閉包,你可以查看文章 理解 JavaScript 閉包。
在文章 理解 JavaScript this 中,我們提到 new 也能改變 this 的指向,那如果 new 和 bind() 同時出現,this 會聽從誰?
function Student() { console.log(this.name, this.age); } Student.prototype.name = "Neil"; Student.prototype.age = 20; var foo = Student.bind({ name: "Leo", age: 21 }); foo(); // "Leo" 21 new foo(); // "Neil" 20
從例子中已經可以看出,使用 new 改變了 bind() 已經綁定的 this 指向,而我們自己的 bound() 函數則不會:
var foo = Student.bound({ name: "Leo", age: 21 }); foo(); // "Leo" 21 new foo(); // "Leo" 21
所以我們還要接著改進 bound() 函數。要解決這個問題,我們需要清楚原型鏈以及 new 的原理,在后面的文章中我再來分析,這里只提供解決方案。
var slice = Array.prototype.slice; Function.prototype.bound = function(ctx) { if (typeof this !== "function") { throw TypeError("Function.prototype.bound - what is trying to be bound is not callable"); } var fn = this; var _args = slice.call(arguments); var fBound = function() { var args = _args.concat(slice.call(arguments)); // 在綁定原函數 fn 時增加一次判斷,如果 this 是 fBound 的一個實例 // 那么此時 fBound 的調用方式一定是 new 調用 // 所以,this 直接綁定 this(fBound 的實例對象) 就好 // 否則,this 依舊綁定到我們指定的 ctx 上 return fn.apply(this instanceof fBound ? this : ctx, args); }; // 這里我們必須要聲明 fBound 的 prototype 指向為原函數 fn 的 prototype fBound.prototype = Object.create(fn.prototype); return fBound; }
大功告成。如果看不懂最后一段代碼,可以先放一放,后面的文章會分析原型鏈和 new 的原理。
◆ call() 的實現function foo() { console.log(this.bar); } var obj = { bar: "baz" }; foo.call(obj); // "baz"
我們觀察 call 的調用,存在下面的特點:
當函數 foo 調用 call,并傳入 obj 時,似乎是在 obj 的原型上增加了一個 foo 方法。
foo.call() 除第一個參數外的所有參數都應該傳給 foo(),這一點在實現 bind() 時已處理過。
不能對 foo 和 obj 做任何修改。
那就來看看,以示區別,我們自己實現的 call 叫做 calling。
Function.prototype.calling = function(ctx) { ctx.fn = this; ctx.fn(); }
我們完成了第一步。
在完成第二步時,我們需要用到 eval(),它可以執行一段字符串類型的 JavaScript 代碼。
var slice = Array.prototype.slice; Function.prototype.calling = function(ctx) { ctx.fn = this; var args = []; for (var i = 1; i < args.length; i++) { args.push("arguments[" + i + "]"); } eval("ctx.fn(" + args + ")"); }
這里我們避免采用和實現 bind() 同樣的方法獲取剩余參數,因為要使用到 call,所以這里采用循環。我們需要一個一個的將參數傳入 ctx.fn(),所以就用到 eval(),這里的 eval() 中的代碼在做 + 運算時,args 會發生類型轉換,自動調用 toString() 方法。
實現到這里,大部分的功能以及完成,但是我們不可避免的為 ctx 手動添加了一個 fn 方法,改變了 ctx 本身,所以要把它給刪除掉。另外,call 應該有返回值,且它的值是 fn 執行過后的結果,并且如果 ctx 傳入 null 或者 undefined,應該將 this 綁定到全局對象。我們可以得到下面的代碼:
var slice = Array.prototype.slice; Function.prototype.calling = function(ctx) { ctx = ctx || window || global; ctx.fn = this; var args = []; for (var i = 1; i < args.length; i++) { args.push("arguments[" + i + "]"); } var result = eval("ctx.fn(" + args + ")"); delete ctx.fn; return result; }◆ apply() 的實現
apply() 的實現與 call() 類似,只是參數的處理不同,直接看代碼吧。
var slice = Array.prototype.slice; Function.prototype.applying = function(ctx, arr) { ctx = ctx || window || global; ctx.fn = this; var result = null; var args = []; if (!arr) { result = ctx.fn(); } else { for (var i = 1; i < args.length; i++) { args.push("arr[" + i + "]"); } result = eval("ctx.fn(" + args + ")"); } delete ctx.fn; return result; }小結
這篇文章在上一篇文章的基礎上,更進一步地討論了 call() / apply() / bind() 的用法以及實現,其中三者的區別和 bind() 的實現是校招面試的常考點,初次接觸可能有點難理解 bind(),因為它涉及到閉包、new 以及原型鏈。
我會在接下來的文章中介紹對象、原型以及原型鏈、繼承、new 的實現原理,敬請期待。
本文原文發布在公眾號 cameraee,點擊查看
文章參考Function.prototype.call() / apply() / bind() | MDN
Invoking JavaScript Functions With "call" and "apply" | A Drop of JavaScript
Implement your own - call(), apply() and bind() method in JavaScript | Ankur Anand
JavaScript .call() .apply() and .bind() - explained to a total noob | Owen Yang
JavaScript call() & apply() vs bind()? | Stack Overflow
Learn & Solve: call(), apply() and bind() methods in JavaScript
JavaScript 系列文章理解 JavaScript this
理解 JavaScript 閉包
理解 JavaScript 執行棧
理解 JavaScript 作用域
理解 JavaScript 數據類型與變量
Be Good. Sleep Well. And Enjoy.
前端技術 | 個人成長
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/100639.html
摘要:輸出的作用與和一樣,都是可以改變函數運行時上下文,區別是和在調用函數之后會立即執行,而方法調用并改變函數運行時上下文后,返回一個新的函數,供我們需要時再調用。 前言 js中的call(), apply()和bind()是Function.prototype下的方法,都是用于改變函數運行時上下文,最終的返回值是你調用的方法的返回值,若該方法沒有返回值,則返回undefined。這幾個方法...
摘要:,,和都是用來改變函數執行時的上下文也就是說改變的指向問題,是的方法,引入是因為沒有將設置成行參。一般都是庫里面用不推薦自己使用和。和唯一區別是參數不一樣,是的語法糖是返回一個新函數供以后調用,相比其他兩個比較常用。而和是立即調用。 apply(),call(),和bind()都是用來改變函數執行時的上下文也就是說改變this的指向問題,是prototype的方法,引入是因為js沒有將...
摘要:它們有明確的和成員函數的定義,只有的實例才能調用這個的成員函數。用和調用函數里用和來指定函數調用的,即指針的指向。同樣,對于一個后的函數使用或者,也無法改變它的執行,原理和上面是一樣的。 函數里的this指針 要理解call,apply和bind,那得先知道JavaScript里的this指針。JavaScript里任何函數的執行都有一個上下文(context),也就是JavaScri...
總結call,apply,bind方法的理解使用和區別。 call,apply,bind這三個方法在JavaScript中是用來改變函數調用的this指向。那么改變函數this指向有什么用呢?我們先來看一段代碼 var a= { name:harden, fn:function () { console.log(this.name); } } var b =...
在上一篇文章(《javascript高級程序設計》筆記:Function類型)中稍微提及了一下函數對象的屬性—this,在這篇文章中有深入的說明: 函數的三種簡單調用模式 1 函數模式 定義的函數,如果單獨調用,不將其與任何對象關聯,那么就是函數調用模式 function fn(num1, num2) { console.log(this); } // 直接在全局調用 fn();// w...
閱讀 4122·2022-09-16 13:49
閱讀 1398·2021-11-22 15:12
閱讀 1519·2021-09-09 09:33
閱讀 1039·2019-08-30 13:15
閱讀 1720·2019-08-29 15:30
閱讀 654·2019-08-27 10:52
閱讀 2643·2019-08-26 17:41
閱讀 1896·2019-08-26 12:11