摘要:理解文章中已經(jīng)比較全面的分析了在中的指向問題,用一句話來總結(jié)就是的指向一定是在執(zhí)行時(shí)決定的,指向被調(diào)用函數(shù)的對(duì)象。與和直接執(zhí)行原函數(shù)不同的是,返回的是一個(gè)新函數(shù)。這個(gè)新函數(shù)包裹了原函數(shù),并且綁定了的指向?yàn)閭魅氲摹?/p>
理解 JavaScript this 文章中已經(jīng)比較全面的分析了 this 在 JavaScript 中的指向問題,用一句話來總結(jié)就是:this 的指向一定是在執(zhí)行時(shí)決定的,指向被調(diào)用函數(shù)的對(duì)象。當(dāng)然,上篇文章也指出可以通過 call() / apply() / bind() 這些內(nèi)置的函數(shù)方法來指定 this 的指向,以達(dá)到開發(fā)者的預(yù)期,而這篇文章將進(jìn)一步來討論這個(gè)問題。
先來回顧一下,舉個(gè)簡(jiǎn)單的例子:
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 中,函數(shù)也是對(duì)象,所以 JS 的函數(shù)有一些內(nèi)置的方法,就包括 call(), apply() 和 bind(),它們都定義在 Function 的原型上,所以每一個(gè)函數(shù)都可以調(diào)用這 3 個(gè)方法。
Function.prototype.call(thisArg [, arg1 [, arg2, ...]]),對(duì)于 call() 而言,它的第一個(gè)參數(shù)為需要綁定的對(duì)象,也就是 this 指向的對(duì)象,比如今天的引例中就是這樣。
第一個(gè)參數(shù)也可以是 null 和 undefined,在嚴(yán)格模式下 this 將指向?yàn)g覽器中的 window 對(duì)象或者是 Node.js 中的 global 對(duì)象。
var leo = { name: "Leo", sayHi: function() { return "Hi! I"m " + this.name; } }; leo.sayHi.call(null); // "Hi! I"m undefined"
▲ this 指向 window,window.name 沒有定義
除了第一個(gè)參數(shù),call() 還可以選擇接收剩下任意多的參數(shù),這些參數(shù)都將作為調(diào)用函數(shù)的參數(shù),來看一下:
function add(a, b) { return a + b; } add.call(null, 2, 3); // 5
▲ 等同于 add(2, 3)
apply() 的用法和 call() 類似,唯一的區(qū)別是它們接收參數(shù)的形式不同。除了第一個(gè)參數(shù)外,call() 是以枚舉的形式傳入一個(gè)個(gè)的參數(shù),而 apply() 是傳入一個(gè)數(shù)組。
function add(a, b) { return a + b; } add.apply(null, [2, 3]); // 5
注意:apply() 接受的第二個(gè)參數(shù)為數(shù)組(也可以是一個(gè)類數(shù)組對(duì)象),但不意味著調(diào)用它的函數(shù)接收的是數(shù)組參數(shù)。這里的 add() 函數(shù)依舊是 a 和 b 兩個(gè)參數(shù),分別賦值為 2 和 3,而不是 a 被賦值為 [2, 3]。
接下來說說 bind(),它和另外兩個(gè)大有區(qū)別。
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() 直接執(zhí)行原函數(shù)不同的是,bind() 返回的是一個(gè)新函數(shù)。簡(jiǎn)單說,bind() 的作用就是將原函數(shù)的 this 綁定到指定對(duì)象,并返回一個(gè)新的函數(shù),以延遲原函數(shù)的執(zhí)行,這在異步流程中(比如回調(diào)函數(shù),事件處理程序)具有很強(qiáng)大的作用。你可以將 bind() 的過程簡(jiǎn)單的理解為:
function bind(fn, ctx) { return function() { fn.apply(ctx, arguments); }; }如何實(shí)現(xiàn)
這一部分應(yīng)該是經(jīng)常出現(xiàn)在面試中。最常見的應(yīng)該是 bind() 的實(shí)現(xiàn),就先來說說如何實(shí)現(xiàn)自己的 bind()。
◆ bind() 的實(shí)現(xiàn)上一節(jié)已經(jīng)簡(jiǎn)單地實(shí)現(xiàn)了一個(gè) bind(),稍作改變,為了和內(nèi)置的 bind() 區(qū)別,我么自己實(shí)現(xiàn)的函數(shù)叫做 bound(),先看一下:
Function.prototype.bound = function(ctx) { var fn = this; return function() { return fn.apply(ctx); }; }
這里的 bound() 模擬了一個(gè)最基本的 bind() 函數(shù)的實(shí)現(xiàn),即返回一個(gè)新函數(shù)。這個(gè)新函數(shù)包裹了原函數(shù),并且綁定了 this 的指向?yàn)閭魅氲?ctx。
對(duì)于內(nèi)置的 bind() 來說,它還有一個(gè)特點(diǎn):
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"
在這里兩次調(diào)用參數(shù)傳遞的方式不同,但是具有同樣的結(jié)果。下面,就繼續(xù)完善我們自己的 bound() 函數(shù)。
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 類數(shù)組對(duì)象轉(zhuǎn)為數(shù)組。我們用一個(gè)變量保存?zhèn)魅?bound() 的除第一個(gè)參數(shù)以外的參數(shù),在返回的新函數(shù)中,將傳入新函數(shù)的參數(shù)與 bound() 中的參數(shù)合并。
其實(shí),到現(xiàn)在整個(gè) bound() 函數(shù)的實(shí)現(xiàn)都離不開閉包,你可以查看文章 理解 JavaScript 閉包。
在文章 理解 JavaScript this 中,我們提到 new 也能改變 this 的指向,那如果 new 和 bind() 同時(shí)出現(xiàn),this 會(huì)聽從誰?
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
從例子中已經(jīng)可以看出,使用 new 改變了 bind() 已經(jīng)綁定的 this 指向,而我們自己的 bound() 函數(shù)則不會(huì):
var foo = Student.bound({ name: "Leo", age: 21 }); foo(); // "Leo" 21 new foo(); // "Leo" 21
所以我們還要接著改進(jìn) bound() 函數(shù)。要解決這個(gè)問題,我們需要清楚原型鏈以及 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)); // 在綁定原函數(shù) fn 時(shí)增加一次判斷,如果 this 是 fBound 的一個(gè)實(shí)例 // 那么此時(shí) fBound 的調(diào)用方式一定是 new 調(diào)用 // 所以,this 直接綁定 this(fBound 的實(shí)例對(duì)象) 就好 // 否則,this 依舊綁定到我們指定的 ctx 上 return fn.apply(this instanceof fBound ? this : ctx, args); }; // 這里我們必須要聲明 fBound 的 prototype 指向?yàn)樵瘮?shù) fn 的 prototype fBound.prototype = Object.create(fn.prototype); return fBound; }
大功告成。如果看不懂最后一段代碼,可以先放一放,后面的文章會(huì)分析原型鏈和 new 的原理。
◆ call() 的實(shí)現(xiàn)function foo() { console.log(this.bar); } var obj = { bar: "baz" }; foo.call(obj); // "baz"
我們觀察 call 的調(diào)用,存在下面的特點(diǎn):
當(dāng)函數(shù) foo 調(diào)用 call,并傳入 obj 時(shí),似乎是在 obj 的原型上增加了一個(gè) foo 方法。
foo.call() 除第一個(gè)參數(shù)外的所有參數(shù)都應(yīng)該傳給 foo(),這一點(diǎn)在實(shí)現(xiàn) bind() 時(shí)已處理過。
不能對(duì) foo 和 obj 做任何修改。
那就來看看,以示區(qū)別,我們自己實(shí)現(xiàn)的 call 叫做 calling。
Function.prototype.calling = function(ctx) { ctx.fn = this; ctx.fn(); }
我們完成了第一步。
在完成第二步時(shí),我們需要用到 eval(),它可以執(zhí)行一段字符串類型的 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 + ")"); }
這里我們避免采用和實(shí)現(xiàn) bind() 同樣的方法獲取剩余參數(shù),因?yàn)橐褂玫?call,所以這里采用循環(huán)。我們需要一個(gè)一個(gè)的將參數(shù)傳入 ctx.fn(),所以就用到 eval(),這里的 eval() 中的代碼在做 + 運(yùn)算時(shí),args 會(huì)發(fā)生類型轉(zhuǎn)換,自動(dòng)調(diào)用 toString() 方法。
實(shí)現(xiàn)到這里,大部分的功能以及完成,但是我們不可避免的為 ctx 手動(dòng)添加了一個(gè) fn 方法,改變了 ctx 本身,所以要把它給刪除掉。另外,call 應(yīng)該有返回值,且它的值是 fn 執(zhí)行過后的結(jié)果,并且如果 ctx 傳入 null 或者 undefined,應(yīng)該將 this 綁定到全局對(duì)象。我們可以得到下面的代碼:
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() 的實(shí)現(xiàn)
apply() 的實(shí)現(xiàn)與 call() 類似,只是參數(shù)的處理不同,直接看代碼吧。
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; }小結(jié)
這篇文章在上一篇文章的基礎(chǔ)上,更進(jìn)一步地討論了 call() / apply() / bind() 的用法以及實(shí)現(xiàn),其中三者的區(qū)別和 bind() 的實(shí)現(xiàn)是校招面試的??键c(diǎn),初次接觸可能有點(diǎn)難理解 bind(),因?yàn)樗婕暗介]包、new 以及原型鏈。
我會(huì)在接下來的文章中介紹對(duì)象、原型以及原型鏈、繼承、new 的實(shí)現(xiàn)原理,敬請(qǐng)期待。
本文原文發(fā)布在公眾號(hào) cameraee,點(diǎn)擊查看
文章參考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 執(zhí)行棧
理解 JavaScript 作用域
理解 JavaScript 數(shù)據(jù)類型與變量
Be Good. Sleep Well. And Enjoy.
前端技術(shù) | 個(gè)人成長(zhǎng)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/100639.html
摘要:輸出的作用與和一樣,都是可以改變函數(shù)運(yùn)行時(shí)上下文,區(qū)別是和在調(diào)用函數(shù)之后會(huì)立即執(zhí)行,而方法調(diào)用并改變函數(shù)運(yùn)行時(shí)上下文后,返回一個(gè)新的函數(shù),供我們需要時(shí)再調(diào)用。 前言 js中的call(), apply()和bind()是Function.prototype下的方法,都是用于改變函數(shù)運(yùn)行時(shí)上下文,最終的返回值是你調(diào)用的方法的返回值,若該方法沒有返回值,則返回undefined。這幾個(gè)方法...
摘要:,,和都是用來改變函數(shù)執(zhí)行時(shí)的上下文也就是說改變的指向問題,是的方法,引入是因?yàn)闆]有將設(shè)置成行參。一般都是庫(kù)里面用不推薦自己使用和。和唯一區(qū)別是參數(shù)不一樣,是的語法糖是返回一個(gè)新函數(shù)供以后調(diào)用,相比其他兩個(gè)比較常用。而和是立即調(diào)用。 apply(),call(),和bind()都是用來改變函數(shù)執(zhí)行時(shí)的上下文也就是說改變this的指向問題,是prototype的方法,引入是因?yàn)閖s沒有將...
摘要:它們有明確的和成員函數(shù)的定義,只有的實(shí)例才能調(diào)用這個(gè)的成員函數(shù)。用和調(diào)用函數(shù)里用和來指定函數(shù)調(diào)用的,即指針的指向。同樣,對(duì)于一個(gè)后的函數(shù)使用或者,也無法改變它的執(zhí)行,原理和上面是一樣的。 函數(shù)里的this指針 要理解call,apply和bind,那得先知道JavaScript里的this指針。JavaScript里任何函數(shù)的執(zhí)行都有一個(gè)上下文(context),也就是JavaScri...
總結(jié)call,apply,bind方法的理解使用和區(qū)別。 call,apply,bind這三個(gè)方法在JavaScript中是用來改變函數(shù)調(diào)用的this指向。那么改變函數(shù)this指向有什么用呢?我們先來看一段代碼 var a= { name:harden, fn:function () { console.log(this.name); } } var b =...
在上一篇文章(《javascript高級(jí)程序設(shè)計(jì)》筆記:Function類型)中稍微提及了一下函數(shù)對(duì)象的屬性—this,在這篇文章中有深入的說明: 函數(shù)的三種簡(jiǎn)單調(diào)用模式 1 函數(shù)模式 定義的函數(shù),如果單獨(dú)調(diào)用,不將其與任何對(duì)象關(guān)聯(lián),那么就是函數(shù)調(diào)用模式 function fn(num1, num2) { console.log(this); } // 直接在全局調(diào)用 fn();// w...
閱讀 4295·2022-09-16 13:49
閱讀 1430·2021-11-22 15:12
閱讀 1556·2021-09-09 09:33
閱讀 1064·2019-08-30 13:15
閱讀 1763·2019-08-29 15:30
閱讀 709·2019-08-27 10:52
閱讀 2675·2019-08-26 17:41
閱讀 1935·2019-08-26 12:11