JS面試必備之手寫call/apply/bind/new
一、Function.prototype.call()
call() 方法用于指定一個 this 值和單獨給出的一個或多個參數(shù)來調用一個函數(shù)。
該方法的語法和作用與 apply() 類似,只有一個區(qū)別,就是 call() 方法接受一個參數(shù)列表,而 apply() 方法接受的是一個包含多個參數(shù)的數(shù)組。
語法:
function.call(thisArg, arg1, arg2, ...)
參數(shù):
- thisArg: 可選的。在 function 函數(shù)運行時使用的 this值。請注意,this 可能不是該方法看到的實際值:如果這個函數(shù)在非嚴格模式下,則指定為 null 或
- undefined 時會自動替換為指向全局對象,原始值會被包裝。
- arg1,arg2,...: 指定的參數(shù)列表。
返回值:
使用調用者提供的的 this 值和參數(shù)調用該函數(shù)的返回值。若該方法沒有返回值,則返回 undefined。
描述:
call() 允許為不同的對象分配和調用屬于一個對象的函數(shù)/方法。
使用:
var foo = { value: 1 }; function bar() { console.log(this.value); } bar.call(foo); // 1
這里需要注意兩點:
- call 函數(shù)改變了 this 的指向,指向了 foo;
- bar 函數(shù)執(zhí)行了;
那我們如何模擬實現(xiàn)呢?我們把代碼改造如下:
var foo = { value: 1, bar() { console.log(this.value); } }; foo.bar(); // 1
這個時候 this 指向了 foo,但是卻給 foo 對象添加了一個屬性,只要思想不滑坡,方法總比困難多,我們用 delete 把它再刪除不就好了嗎?
1. 把函數(shù)設置為對象的屬性;
2. 執(zhí)行函數(shù);
3. 從對象中刪除函數(shù);
代碼如下:
Function.prototype.call3 = function(context) { // 第一步 context.fn = this; // 第二步 context.fn(); // 第三步 delete context.fn; }
測試代碼:
var foo = { value: 1 }; ???????function bar() { console.log(this.value); } bar.call3(foo); // 1
第一步:這里的 context 為 foo,this 為 函數(shù) bar,把 bar 函數(shù)賦值為 foo 的一個對象屬性,即:foo.fn = bar;
第二步:執(zhí)行 foo.fn 函數(shù);
第三步:刪除 foo.fn 函數(shù);
當然,我們也可以給 call 函數(shù)傳遞參數(shù),函數(shù)也可以有返回值,如下:
var value = 'global'; // a var foo = { value: 1 }; function bar(name,age) { console.log(this.value); // b console.log(name); console.log(age); return { age, name }; } bar.call(this, 'tom', 20); // 1 tom 20 var result = bar.call(null, 'tom', 20); // global tom 20 // c console.log(result); // {age: 20, name: "tom"}
this 參數(shù)可以傳 null,當為 null 的時候,視為指向 window;
函數(shù)可以有返回值;
注意:a 處變量的聲明要用 var 而不是 let,否則在 c 處調用時,this 為 null 時,b 處的輸出結果會是 undefined,這里主要是在全局聲明時, 與 var 關鍵字不同,使用 let 在全局作用域中聲明的變量不會成為 window 對象的屬性(var 聲明的則會)。
實現(xiàn)代碼如下:
Function.prototype.call3 = function(context) { context = context || window; let args = [...arguments].slice(1); // 第一步 context.fn = this; // 第二步 let result = context.fn(...args); // 第三步 delete context.fn; return result; }
測試代碼如下:
Function.prototype.call3 = function(context) { context = context || window; let args = [...arguments].slice(1); // 第一步 context.fn = this; // 第二步 let result = context.fn(...args); // 第三步 delete context.fn; return result; }
二、Function.prototype.apply()
apply() 方法抵用一個具有給定 this 值的函數(shù),以及一個數(shù)組(或一個類數(shù)組對象)的形式提供的參數(shù)。
語法:
pply(thisArg)
apply(thisArg, argsArray)
參數(shù):
- thisArg: 在 func 函數(shù)運行時使用的 this 值,請注意,this 可能不是該方法看到的實際值:如果這個函數(shù)在非嚴格模式下,則指定為 null 或 undefined 時會自動替換為指向全局對象,原始值會被包裝。
- argsArray: 可選的,一個數(shù)組或者類數(shù)組對象,其中的數(shù)組元素將作為單獨的參數(shù)傳給 func 函數(shù)。如果該參數(shù)的值為null 或者 undefined,則表示不需要傳遞任何參數(shù)。
返回值:
調用有指定 this 值和參數(shù)的函數(shù)的結果。
描述:
apply 與 call()非常相似,不同之處在于提供參數(shù)的方式。apply 使用參數(shù)數(shù)組而不是一組參數(shù)列表。apply 可以使用數(shù)組字面量(array literal),如 fun.apply(this, ['eat', 'bananas']),或數(shù)組對象,如 fun.apply(this, new Array('eat', 'bananas'))。
你也可以使用 arguments 對象作為 argsArray 參數(shù)。arguments 是一個函數(shù)的局部變量。它可以被用作被調用對象的所有未指定的參數(shù)。這樣,你在使用 apply 函數(shù)的時候就不需要知道被調用對象的所有參數(shù)。你可以使用 arguments 來把所有的參數(shù)傳遞給被調用對象。被調用對象接下來就負責處理這些參數(shù)。
備注:雖然這個函數(shù)的語法與 call() 幾乎相同,但根本區(qū)別在于,call() 接受一個參數(shù)列表,而 apply() 接受一個參數(shù)的單數(shù)組。
apply 的實現(xiàn)其實跟 call 差不多,話不多說,直接上代碼:
Function.prototype.apply2 = function(context) { context = context || window; let args = [...arguments][1]; // 注意,這里call 傳遞的參數(shù)是一個數(shù)組,直接取數(shù)組下標第二位就可以了 context.fn = this; let result = context.fn(...args); delete context.fn; return result; }
測試代碼:
var value = 'global'; var foo = { value: 1 }; function bar(name,age) { console.log(this.value); console.log(name); console.log(age); return { name, age, }; } var result = bar.apply2(foo, ['tom', 20]); // 1 tom 20 var result2 = bar.apply2(null, ['tom', 20]); // global tom 20 console.log(result); // {age: 20, name: "tom"}
三、Function.prototype.bind()
bind() 方法創(chuàng)建一個新的函數(shù),在 bind() 被調用時,這個新函數(shù)的 this 被指定為 bind() 的第一個參數(shù),而其余參數(shù)將作為新函數(shù)的參數(shù),供調用時使用。
語法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
參數(shù):
- thisArg: 調用綁定函數(shù)時作為 this 參數(shù)傳遞給目標函數(shù)的值。如果使用 new 運算符構造綁定函數(shù),則忽略該值。當使用 bind 在 setTimeout 中創(chuàng)建一個函數(shù)(作為回調提供)時,作為 thisArg 傳遞的任何原始值都將轉換為 Object。如果 bind 函數(shù)的參數(shù)列表為空,或者 thisArg 是 null或 undefined,執(zhí)行作用域的 this 將被視為新函數(shù)的 thisArg。
- arg1, arg2, ...: 當目標函數(shù)被調用時,被預置入綁定函數(shù)的參數(shù)列表中的參數(shù)。
返回值:
返回一個原函數(shù)的拷貝,并擁有指定的 this 值和初始參數(shù)。
描述:
bind() 函數(shù)會創(chuàng)建一個新的綁定函數(shù)(bound function,BF)。綁定函數(shù)是一個 exotic function object(怪異函數(shù)),它包裝了元函數(shù)對象。調用綁定函數(shù)通常會導致執(zhí)行包裝函數(shù)。綁定函數(shù)具有如下內部屬性:
- [[BoundTargetFunction]] - 包裝的函數(shù)對象。
- [[BoundThis]] - 在調用包裝函數(shù)時始終作為 this 值傳遞的值。
- [[BoundArguments]] - 列表,在對包裝函數(shù)做任何調用都會優(yōu)先用列表元素填充參數(shù)列表。
- [[Call]] - 執(zhí)行與此對象關聯(lián)的代碼。通過函數(shù)調用表達式調用。內部方法的參數(shù)是一個this值和一個包含通過調用表達式傳遞給函數(shù)的參數(shù)的列表。
綁定函數(shù)也可以使用 new 運算符構造,它會表現(xiàn)為目標函數(shù)已經(jīng)被構建完畢了似的。提供的 this 值會被忽略,但前置參數(shù)仍會提供給模擬函數(shù)。
使用一(創(chuàng)建綁定函數(shù)):
bind 最簡單的用法就是創(chuàng)建一個函數(shù),不論怎么調用,這個函數(shù)都有同樣的 this 值。JavaScript 新手經(jīng)常犯的一個錯誤就是將一個方法從對象中拿出來,然后再調用,期望方法中的 this 是原來的對象。如果不做特殊處理的話,一般會丟失原來的對象?;谶@個函數(shù),用原始的對象創(chuàng)建一個綁定函數(shù),巧妙的解決了這個問題。
this.x = 9; // 在瀏覽器中,this 指向全局的 "window" 對象 var module = { x: 81, getX: function() { return this.x; } }; module.getX(); // 81 var retrieveX = module.getX; retrieveX(); // 返回 9 - 因為函數(shù)是在全局作用域中調用的 // 創(chuàng)建一個新函數(shù),把 'this' 綁定到 module 對象 // 新手可能會將全局變量 x 與 module 的屬性 x 混淆 var boundGetX = retrieveX.bind(module); boundGetX(); // 81
關于制定 this 的指向,我們可以使用 call 或者 apply 來實現(xiàn),關于 call 和 apply 的實現(xiàn),我們在上面已經(jīng)講述過了。
模擬實現(xiàn)一:
Function.prototype.bind1 = function(context) { let that = this; return function() { return that.apply(context); } }
測試代碼: this.x = 9; // 在瀏覽器中,this 指向全局的 "window" 對象 var module = { x: 81, getX: function() { return this.x; } }; var boundGetX = retrieveX.bind1(module); var result = boundGetX(); // 81 console.log(result);
使用二(傳參數(shù)):
bind() 的另一個簡單的使用方法是使一個函數(shù)擁有預設的初始參數(shù),只要這些參數(shù)(如果有的話)作為 bind() 的參數(shù)寫在 this 的后面,當綁定函數(shù)被調用的時候,這些參數(shù)會被插入到目標函數(shù)的參數(shù)列表的開始位置,傳遞給綁定函數(shù),綁定函數(shù)的參數(shù)會跟在他們后面。
function list() { return Array.prototype.slice.call(arguments); } function addArguments(arg1, arg2) { return arg1 + arg2 } var list1 = list(1, 2, 3); // [1, 2, 3] var result1 = addArguments(1, 2); // 3 // 創(chuàng)建一個函數(shù),它擁有預設參數(shù)列表。 var leadingThirtysevenList = list.bind(null, 37); // 創(chuàng)建一個函數(shù),它擁有預設的第一個參數(shù) var addThirtySeven = addArguments.bind(null, 37); var list2 = leadingThirtysevenList(); // [37] var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3] var result2 = addThirtySeven(5); // 37 + 5 = 42 var result3 = addThirtySeven(5, 10); // 37 + 5 = 42,第二個參數(shù)被忽略
接下來我們來實現(xiàn)傳參的模擬:
Function.prototype.bind1 = function(context) { let that = this; // 獲取 bind1 函數(shù)從第二個到最后一個參數(shù) let args = [...arguments].slice(1); return function() { // 這個時候 arguments 是指 bind 返回的函數(shù)的入?yún)? return that.apply(context,[...args].concat([...arguments])); } }
測試代碼:
var foo = { value: 1 }; function bar(name, age) { console.log(this.value); console.log(name); console.log(age); return this.value; } var bindFoo = bar.bind1(foo,'daisy'); bindFoo('18'); // 1
使用三(new 構造函數(shù))
bind 還有一個特點就是,一個綁定函數(shù)也能使用 new 操作符創(chuàng)建對象,這種行為就像把原函數(shù)當做構造器,提供的 this 將被忽略,同時調用時的參數(shù)被提供給模擬函數(shù)。
var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; var bindFoo = bar.bind(foo, 'daisy'); var obj = new bindFoo('18'); // undefined // daisy // 18 console.log(obj.habit); console.log(obj.friend); // shopping // kevin
盡管在全局和 foo 中都聲明了 value 值,最后依然返回了 undefined,說明綁定的 this 值失效了,在下面將會見到 new 的模擬實現(xiàn),就會知道這個時候 this 已經(jīng)指向了 obj。
下面我們來模擬實現(xiàn)一下:
Function.prototype.bind2 = function(context) { let that = this; // let args = [...arguments].slice(1); let args = Array.prototype.slice.call(arguments,1); var fBound = function() { let bindArgs = Array.prototype.slice.call(arguments); // 當作為構造函數(shù)時,this 指向實例,此時 this instanceof fBound 為 true return that.apply(this instanceof fBound ? this : context,args.concat(bindArgs)); }; // 修改返回函數(shù)的 prototype 為綁定函數(shù)的 prototype,實例就可以繼承綁定函數(shù)中的原型 fBound.prototype = this.prototype; return fBound; }
測試代碼如下:
var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; var bindFoo = bar.bind2(foo, 'daisy'); var obj = new bindFoo('18'); // undefined // daisy // 18 console.log(obj.habit); console.log(obj.friend);
上面的 fBound.prototype = this.prototype 有一個缺點,直接修改 fBound.prototype 的時候,也會修改 this.prototype,因為他們是引用同一個地址。
如下:
var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; var bindFoo = bar.bind2(foo, 'Jack'); // bind2 var obj = new bindFoo(20); // 返回正確 // undefined // Jack // 20 obj.habit; // 返回正確 // shopping obj.friend; // 返回正確 // kevin obj.__proto__.friend = "Kitty"; // 修改原型 bar.prototype.friend; // 返回錯誤,這里被修改了 // Kitty
解決方案就是使用一個空對象作為中介,把 fBound.prototype 賦值為空對象的實例。
var fNOP = function () {}; // 創(chuàng)建一個空對象 fNOP.prototype = this.prototype; // 空對象的原型指向綁定函數(shù)的原型 fBound.prototype = new fNOP(); // 空對象的實例賦值給 fBound.prototype
最終實現(xiàn)效果如下:
Function.prototype.bind2 = function(context) { let that = this; // let args = [...arguments].slice(1); let args = Array.prototype.slice.call(arguments,1); var fNOP = function () {}; var fBound = function() { let bindArgs = Array.prototype.slice.call(arguments); // 當作為構造函數(shù)時,this 指向實例,此時 this instanceof fBound 為 true return that.apply(this instanceof fBound ? this : context,args.concat(bindArgs)); }; // 修改返回函數(shù)的 prototype 為綁定函數(shù)的 prototype,實例就可以繼承綁定函數(shù)中的原型 fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }
四、new 操作符
使用 new 操作符實例化一個類等于使用 new 調用其構造函數(shù)。唯一可感知的不同之處就是,JavaScript 解釋器知道使用 new 和類意味著該使用 construct 函數(shù)進行實例化。
使用 new 調用類的構造函數(shù)會執(zhí)行如下操作:
- 在內存中創(chuàng)建一個新的對象;
- 改變新對象 proto 指向 構造函數(shù)的 prototype 屬性;
- 構造函數(shù)內部的 this 被賦值為這個新對象;
- 執(zhí)行構造函數(shù)內部的代碼;
- 如果構造函數(shù)返回 非空對象,則返回該對象,否則,返回剛才創(chuàng)建的新對象。
new 的實現(xiàn)一:
function newCreate() { let obj = new Object(); Constructor = Array.prototype.shift.call(arguments); obj.__proto__ = Constructor.prototype; Constructor.apply(obj, arguments); return obj; }
在這里我們做了以下事情:
- 創(chuàng)建一個新的對象;
- 取出第一個參數(shù),這就是我們要傳入的構造函數(shù),因為 shift 會修改原數(shù)組,所以 arguments 會被去除第一個參數(shù);
- 將 obj 的 proto 指向構造函數(shù)的 prototype;,這樣 obj 就可以訪問到構造函數(shù)原型中的屬性;
- 使用 apply 改變構造函數(shù) this 的指向,這樣 obj 就可以訪問到構造函數(shù)中的屬性;
- 返回 obj;
測試代碼如下:
function Person(name,age) { this.name = name; this.age = age; } Person.prototype.printName = function() { console.log(this.name); }
但是我們還需要注意,如果構造函數(shù)返回非空對象,則返回該對象,否則,返回剛才創(chuàng)建的新對象。那么這句話怎么理解呢?請看下面的示例:
function Person(name,age) { this.name = name; this.age = age; return { name, gender: 'male', } } Person.prototype.printName = function() { console.log(this.name); } let p = new Person('tom', 18); console.log(p.name); // tom console.log(p.age); // undefined
在這個例子中,構造函數(shù)返回了一個對象,在實例 p 中就只能訪問到返回的對象中的屬性,也就是說只能訪問到 name 和 gender, age 屬性是訪問不到的。注意,這里返回的是一個對象,如果返回的是一個基本類型數(shù)據(jù)呢?
function Person(name,age) { this.name = name; this.age = age; return 20; } Person.prototype.printName = function() { console.log(this.name); } let p = new Person('tom', 18); console.log(p.name); // tom console.log(p.age); // 20
在這里返回的是一個基本類型的數(shù)據(jù),盡管有返回值,相當于沒有返回。
所以,下面我們需要判斷一下構造函數(shù)的返回值是基本類型數(shù)據(jù)還是一個一個對象。如果是一個對象,我們就返回這個對象,如果沒有,我們就不做任何處理。
function newCreate() { let obj = new Object(); Constructor = Array.prototype.shift.call(arguments); obj.__proto__ = Constructor.prototype; let result = Constructor.apply(obj, arguments); return typeof result === 'object' ? result : obj; }
測試代碼:
返回一個基本類型數(shù)據(jù):
function Person(name,age) { this.name = name; this.age = age; return 20; } let p1 = newCreate(Person,'rose',20); console.log(p1.name); // rose console.log(p1.age); // 20 console.log(p1.gender); // undefined
返回一個對象:
function Person(name,age) { this.name = name; this.age = age; return { name, gender: 'male', } } let p1 = newCreate(Person,'rose',20); console.log(p1.name); // rose console.log(p1.age); // undefined console.log(p1.gender); // male
以上就是JS面試必備之手寫call/apply/bind/new的詳細內容,更多關于JS手寫call apply bind new的資料請關注腳本之家其它相關文章!
相關文章
原生JavaScript實現(xiàn)todolist功能
本篇文章給大家介紹了通過原生JavaScript實現(xiàn)todolist功能相關知識點,對此有需要的朋友可以學習下。2018-03-03js獲取TreeView控件選中節(jié)點的Text和Value值的方法
在實際項目中,遇到一個問題,首先彈出一個新窗口,新窗口中放了一個TreeView控件,現(xiàn)在要解決的是,如何單擊TreeView中一個節(jié)點,返回Text和Value到父頁面并關閉該新窗口,本文將詳細介紹此方法的實現(xiàn)2012-11-11