Javascript的New、Apply、Bind、Call知多少
1 寫在前面
Javascript中的apply、call、bind方法是前端代碼開發(fā)中相當重要的概念,并且與this的指向密切相關(guān)。本篇文章我們將深入探討這個關(guān)鍵詞的作用,并嘗試進行手動編寫復(fù)現(xiàn)。
閱讀文章前,我們帶著幾個問題進行研究:
- 用什么樣的思路可以new關(guān)鍵字?
- apply、bind、call這三個方法有什么區(qū)別?
- 怎樣手動實現(xiàn)一個apply、bind和call?
2 new關(guān)鍵詞
new關(guān)鍵詞的作用是執(zhí)行一個構(gòu)造函數(shù),返回一個實例對象,根據(jù)構(gòu)造函數(shù)的情況來確定是否可以接受參數(shù)的傳遞。
2.1 new的原理
使用new進行實例化對象,其步驟是:
- 創(chuàng)建一個新的空對象,即{}
- 將該對象構(gòu)造函數(shù)的作用域賦給新對象,this指向新對象(即將新對象作為this的上下文)
- 執(zhí)行構(gòu)造函數(shù)中的代碼,為這個新對象添加屬性
- 如果該對象構(gòu)造函數(shù)沒有返回對象,則返回this
- function Person(){
- this.name = "yichuan"
- }
- const p = new Person();
- console.log(p.name);//"yichuan"
我們可以看到當使用new進行實例化時,可以將構(gòu)造函數(shù)的this指向新對象p。當不使用new時,此時構(gòu)造函數(shù)的this指向window。
- function Person(){
- this.name = "yichuan"
- }
- const p = Person();
- console.log(p);//undefined
- console.log(name);//"yichuan" window.name
- console.log(p.name);//"yichuan" is undefined
當我們在構(gòu)造函數(shù)中直接返回一個和this無關(guān)的對象時,使用new關(guān)鍵字進行實例化對象,新生成的對象就是構(gòu)造函數(shù)返回的對象,而非構(gòu)造函數(shù)的this的對象。
- function Person(){
- this.name = "yichuan";
- return {age:18};
- }
- const p = new Person();
- console.log(p);//{age:18}
- console.log(p.name);//"undefined"
- console.log(p.age);/18
此外,當構(gòu)造函數(shù)返回的不是一個對象,而是基礎(chǔ)數(shù)據(jù)類型的值時,使用new創(chuàng)建新對象,會將構(gòu)造函數(shù)返回的值以對象形式給新對象。
- function Person(){
- this.name = "yichuan";
- return "onechuan";
- }
- const p = new Person();
- console.log(p);//{name:"yichuan"}
- console.log(p.name);//"yichuan"
new關(guān)鍵詞執(zhí)行之后總是會返回一個對象,要么是實例,要么是return語句指定的對象。
2.2 手寫new的實現(xiàn)
new被調(diào)用后大致做了哪些事情?
- 讓實例可以訪問私有屬性
- 讓實例可以訪問構(gòu)造函數(shù)原型(constructor.prototype)所在原型鏈上的屬性
- 構(gòu)造函數(shù)返回的最后結(jié)果是引用數(shù)據(jù)類型
- function new_object(ctor,...args){
- //先要判斷ctor是否為一個函數(shù)
- if(typeof ctor !== "function"){
- throw "ctor must be a function";
- }
- //創(chuàng)建一個空對象
- const obj = new Object();
- //將實例obj可以訪問到ctor原型所在原型鏈的屬性
- obj.__proto__ = Object.create(ctor.prototype);
- //將構(gòu)造函數(shù)的this指向?qū)嵗龑ο髈bj
- const res = ctor.apply(obj,...args);
- //確保最后new返回的是一個對象
- const isObject = typeof res === "object" && typeof res!== null;
- const isFunction = typeof res === "function";
- return isObject || isFunction ? res : obj;
- }
當然,我們還可以進行優(yōu)化以下:
- function new_object() {
- // 1、獲得構(gòu)造函數(shù),同時刪除 arguments 中第一個參數(shù)
- const ctor = [].shift.call(arguments);//其實這里是借用了數(shù)組的shift方法
- // 2、創(chuàng)建一個空的對象并鏈接到原型,obj 可以訪問構(gòu)造函數(shù)原型中的屬性
- const obj = Object.create(ctor.prototype);
- // 3、綁定 this 實現(xiàn)繼承,obj 可以訪問到構(gòu)造函數(shù)中的屬性
- const ret =ctor.apply(obj, arguments);
- // 4、優(yōu)先返回構(gòu)造函數(shù)返回的對象
- return ret instanceof Object ? ret : obj;
- };
3 apply、bind以及call
apply、bind和call是掛載Function對象上的三個方法,調(diào)用這三個方法的必須是一個函數(shù)。
3.1 apply
apply() 方法調(diào)用一個具有給定 this 值的函數(shù),以及作為一個數(shù)組(或類似數(shù)組對象)提供的參數(shù)。apply()方法可以改變函數(shù)this的指向,且立即執(zhí)行函數(shù)。
注意:Chrome 14 以及 Internet Explorer 9 仍然不接受類數(shù)組對象。如果傳入類數(shù)組對象,它們會拋出異常。
- func.apply(thisArg, [param1,param2,...]);
在使用apply時,會將func的this指向改變?yōu)橹赶騮hisArg,然后以[param1,param2,...]參數(shù)數(shù)組作為參數(shù)輸入。
- func(["red","green","blue"]);
- func.apply(newFun, ["red","green","blue"]);
我們可以看到都執(zhí)行func時,第一個func函數(shù)的this指向的是window全局對象,而第二個func函數(shù)的this指向的是newFun。
- Function.prototype.apply = function (context, arr) {
- context = context ? Object(context) : window;
- context.fn = this;
- let result;
- //判斷有沒有參數(shù)數(shù)組輸入
- if (!arr) {
- result = context.fn();
- } else {
- result = context.fn(...arr);
- }
- //此處也可以使用eval進行處理
- // const result = eval("context.fn(...arr)");
- delete context.fn
- return result;
- }
3.2 bind
bind() 方法創(chuàng)建一個新的函數(shù),在 bind() 被調(diào)用時,這個新函數(shù)的 this 被指定為 bind() 的第一個參數(shù),而其余參數(shù)將作為新函數(shù)的參數(shù),供調(diào)用時使用。
- bind(thisArg,param1,param2,...);
其實,bind的實現(xiàn)思路基本和apply一致,但是在最后實現(xiàn)返回結(jié)果時,bind不需要直接執(zhí)行,而是以返回函數(shù)的形式返回結(jié)果,之后再通過執(zhí)行這個結(jié)果即可。
先分析下bind的特性:首先是指定新對象this指向,再傳入?yún)?shù)返回一個定義的函數(shù),最后使用柯里化進行調(diào)用。同樣的,我們也可以根據(jù)這些特性進行手動封裝一個bind函數(shù):
- Function.prototype.bind = function(context){
- //先要判斷調(diào)用bind函數(shù)的是不是函數(shù),需要拋出異常
- if(typeof this !== "function"){
- throw new Error("this bind function must be userd to function");
- }
- //存儲this的指向
- const self = this;
- //context是新對象this指向的目標對象,而參數(shù)就是在第一個參數(shù)之后的參數(shù)
- const args = Array.prototype.slice.call(arguments,1);
- //創(chuàng)建一個空對象
- const fun = function(){}
- //返回一個函數(shù)
- const funBind = function(){
- //返回所有的參數(shù)給bind函數(shù)
- const bindArg = Array.prototype.slice.call(arguments);
- //將傳入的參數(shù)合并成一個新的參數(shù)數(shù)組,作為self.apply()的第二個參數(shù)
- return self.apply(this instanceof fun ? this : context, args.concat(bindArgs));
- /**********************說明************************************/
- }
- //空對象的原型指向綁定函數(shù)的原型
- fun.prototype = this.prototype;
- //空對象的實例賦值給 funBind.prototype
- funBind.prototype = new fun();
- return funBinf;
- }
補充說明:
- this instanceof fun返回為true時,表示的是fun是一個構(gòu)造函數(shù),其this指向?qū)嵗?,直接將context作為參數(shù)輸入
- this instanceof fun返回為false時,表示的是fun是一個普通函數(shù),其this指向頂級對象window,將綁定函數(shù)的this指向context對象
當然,我們也可以寫成這種形式:
- Function.prototype.bind = function(context,...args){
- //先要判斷調(diào)用bind函數(shù)的是不是函數(shù),需要拋出異常
- if(typeof this !== "function"){
- throw new Error("this bind function must be userd to function");
- }
- //存儲this的指向
- const self = this;
- const fBind = function(){
- self.apply(this instanceof self ? this: context, args.concat(Array.prototype.slice.call(arguments)));
- }
- if(this.prototype){
- fBind.prototype = Object.create(this.prototype);
- }
- return fBind;
- }
注意:Object.create()是es2015語法引入的新特性,因此在IE<9的瀏覽器是不支持的。
3.3 call
call() 方法使用一個指定的 this 值和單獨給出的一個或多個參數(shù)來調(diào)用一個函數(shù)。使用調(diào)用者提供的 this 值和參數(shù)調(diào)用該函數(shù)的返回值。若該方法沒有返回值,則返回 undefined。
- function.call(thisArg, param1, param2, ...)
注意:該方法的語法和作用與 apply() 方法類似,只有一個區(qū)別,就是 call() 方法接受的是一個參數(shù)列表,而 apply() 方法接受的是一個包含多個參數(shù)的數(shù)組。
call函數(shù)的實現(xiàn):
- Function.prototype.call = function(context,...args){
- //將函數(shù)設(shè)置為對象的屬性
- context = context || window;
- context.fn = this;
- //執(zhí)行函數(shù)
- const result = eval("context.fn(...args)");
- //刪除對象的這個屬性
- delete context.fn;
- return result;
- }
4 參考文章
- 《解析 bind 原理,并手寫 bind 實現(xiàn)》
- 《解析 call/apply 原理,并手寫 call/apply 實現(xiàn)》
- 《Javascript核心原理精講》
5 寫在最后
在這篇文章中,我們知道apply、bind、call的區(qū)別在于:
- apply、call改變了this指向后,會立即進行調(diào)用函數(shù),返回的是執(zhí)行結(jié)果
- bind在改變this指向后,返回的是一個函數(shù),需要另外再進行調(diào)用一次
- bind、call傳遞的第一個參數(shù)都是this將要指向的對象,后面都是一個一個參數(shù)的形式輸入
- apply傳遞的第一個參數(shù)也是this將要指向的對象,后面?zhèn)鬟f的第二個參數(shù)是一個參數(shù)數(shù)組