bind()
方法會建立一個新函數,當這個新函數被調用時,它的this
值是傳遞給bind()
的第一個參數,傳入bind方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。bind返回的綁定函數也能使用new
操做符建立對象:這種行爲就像把原函數當成構造器,提供的this
值被忽略,同時調用時的參數被提供給模擬函數。(來自參考1)
語法:fun.bind(thisArg[, arg1[, arg2[, ...]]])
html
bind
方法與 call / apply
最大的不一樣就是前者返回一個綁定上下文的函數,然後二者是直接執行了函數。前端
來個例子說明下webpack
var value = 2; var foo = { value: 1 }; function bar(name, age) { return { value: this.value, name: name, age: age } }; bar.call(foo, "Jack", 20); // 直接執行了函數 // {value: 1, name: "Jack", age: 20} var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一個函數 bindFoo1(); // {value: 1, name: "Jack", age: 20} var bindFoo2 = bar.bind(foo, "Jack"); // 返回一個函數 bindFoo2(20); // {value: 1, name: "Jack", age: 20}
經過上述代碼能夠看出bind
有以下特性:git
this
常常有以下的業務場景github
var nickname = "Kitty"; function Person(name){ this.nickname = name; this.distractedGreeting = function() { setTimeout(function(){ console.log("Hello, my name is " + this.nickname); }, 500); } } var person = new Person('jawil'); person.distractedGreeting(); //Hello, my name is Kitty
這裏輸出的nickname
是全局的,並非咱們建立 person
時傳入的參數,由於 setTimeout
在全局環境中執行(不理解的查看【進階3-1期】),因此 this
指向的是window
。web
這邊把 setTimeout
換成異步回調也是同樣的,好比接口請求回調。面試
解決方案有下面兩種。算法
解決方案1:緩存 this
值跨域
var nickname = "Kitty"; function Person(name){ this.nickname = name; this.distractedGreeting = function() { var self = this; // added setTimeout(function(){ console.log("Hello, my name is " + self.nickname); // changed }, 500); } } var person = new Person('jawil'); person.distractedGreeting(); // Hello, my name is jawil
解決方案2:使用 bind
數組
var nickname = "Kitty"; function Person(name){ this.nickname = name; this.distractedGreeting = function() { setTimeout(function(){ console.log("Hello, my name is " + this.nickname); }.bind(this), 500); } } var person = new Person('jawil'); person.distractedGreeting(); // Hello, my name is jawil
完美!
【進階3-3期】介紹了 call
的使用場景,這裏從新回顧下。
function isArray(obj){ return Object.prototype.toString.call(obj) === '[object Array]'; } isArray([1, 2, 3]); // true // 直接使用 toString() [1, 2, 3].toString(); // "1,2,3" "123".toString(); // "123" 123.toString(); // SyntaxError: Invalid or unexpected token Number(123).toString(); // "123" Object(123).toString(); // "123"
能夠經過toString()
來獲取每一個對象的類型,可是不一樣對象的 toString()
有不一樣的實現,因此經過 Object.prototype.toString()
來檢測,須要以 call() / apply()
的形式來調用,傳遞要檢查的對象做爲第一個參數。
另外一個驗證是不是數組的方法,這個方案的優勢是能夠直接使用改造後的 toStr
。
var toStr = Function.prototype.call.bind(Object.prototype.toString); function isArray(obj){ return toStr(obj) === '[object Array]'; } isArray([1, 2, 3]); // true // 使用改造後的 toStr toStr([1, 2, 3]); // "[object Array]" toStr("123"); // "[object String]" toStr(123); // "[object Number]" toStr(Object(123)); // "[object Number]"
上面方法首先使用 Function.prototype.call
函數指定一個 this
值,而後 .bind
返回一個新的函數,始終將 Object.prototype.toString
設置爲傳入參數。其實等價於 Object.prototype.toString.call()
。
這裏有一個前提是toString()
方法沒有被覆蓋
Object.prototype.toString = function() { return ''; } isArray([1, 2, 3]); // false
只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
能夠一次性地調用柯里化函數,也能夠每次只傳一個參數分屢次調用。
var add = function(x) { return function(y) { return x + y; }; }; var increment = add(1); var addTen = add(10); increment(2); // 3 addTen(2); // 12 add(1)(2); // 3
這裏定義了一個 add
函數,它接受一個參數並返回一個新的函數。調用 add
以後,返回的函數就經過閉包的方式記住了 add
的第一個參數。因此說 bind
自己也是閉包的一種使用場景。
bind()
函數在 ES5 才被加入,因此並非全部瀏覽器都支持,IE8
及如下的版本中不被支持,若是須要兼容可使用 Polyfill 來實現。
首先咱們來實現如下四點特性:
this
對於第 1 點,使用 call / apply
指定 this
。
對於第 2 點,使用 return
返回一個函數。
結合前面 2 點,能夠寫出初版,代碼以下:
// 初版 Function.prototype.bind2 = function(context) { var self = this; // this 指向調用者 return function () { // 實現第 2點 return self.apply(context); // 實現第 1 點 } }
測試一下
// 測試用例 var value = 2; var foo = { value: 1 }; function bar() { return this.value; } var bindFoo = bar.bind2(foo); bindFoo(); // 1
對於第 3 點,使用 arguments
獲取參數數組並做爲 self.apply()
的第二個參數。
對於第 4 點,獲取返回函數的參數,而後同第3點的參數合併成一個參數數組,並做爲 self.apply()
的第二個參數。
// 第二版 Function.prototype.bind2 = function (context) { var self = this; // 實現第3點,由於第1個參數是指定的this,因此只截取第1個以後的參數 // arr.slice(begin); 即 [begin, end] var args = Array.prototype.slice.call(arguments, 1); return function () { // 實現第4點,這時的arguments是指bind返回的函數傳入的參數 // 即 return function 的參數 var bindArgs = Array.prototype.slice.call(arguments); return self.apply( context, args.concat(bindArgs) ); } }
測試一下:
// 測試用例 var value = 2; var foo = { value: 1 }; function bar(name, age) { return { value: this.value, name: name, age: age } }; var bindFoo = bar.bind2(foo, "Jack"); bindFoo(20); // {value: 1, name: "Jack", age: 20}
到如今已經完成大部分了,可是還有一個難點,bind
有如下一個特性
一個綁定函數也能使用new操做符建立對象:這種行爲就像把原函數當成構造器,提供的 this 值被忽略,同時調用時的參數被提供給模擬函數。
來個例子說明下:
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, 'Jack'); var obj = new bindFoo(20); // undefined // Jack // 20 obj.habit; // shopping obj.friend; // kevin
上面例子中,運行結果this.value
輸出爲 undefined
,這不是全局value
也不是foo
對象中的value
,這說明 bind
的 this
對象失效了,new
的實現中生成一個新的對象,這個時候的 this
指向的是 obj
。(【進階3-1期】有介紹new的實現原理,下一期也會重點介紹)
這裏能夠經過修改返回函數的原型來實現,代碼以下:
// 第三版 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); // 註釋1 return self.apply( this instanceof fBound ? this : context, args.concat(bindArgs) ); } // 註釋2 fBound.prototype = this.prototype; return fBound; }
註釋1:
this instanceof fBound
結果爲 true
,可讓實例得到來自綁定函數的值,即上例中實例會具備 habit
屬性。window
,此時結果爲 false
,將綁定函數的 this 指向 context
prototype
爲綁定函數的 prototype
,實例就能夠繼承綁定函數的原型中的值,即上例中 obj
能夠獲取到 bar
原型上的 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 () {}; // 建立一個空對象 fNOP.prototype = this.prototype; // 空對象的原型指向綁定函數的原型 fBound.prototype = new fNOP(); // 空對象的實例賦值給 fBound.prototype
這邊能夠直接使用ES5的 Object.create()
方法生成一個新對象
fBound.prototype = Object.create(this.prototype);
不過 bind
和 Object.create()
都是ES5方法,部分IE瀏覽器(IE < 9)並不支持,Polyfill中不能用 Object.create()
實現 bind
,不過原理是同樣的。
第四版目前OK啦,代碼以下:
// 第四版,已經過測試用例 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fNOP = function () {}; var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply( this instanceof fNOP ? this : context, args.concat(bindArgs) ); } fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }
到這裏其實已經差很少了,但有一個問題是調用 bind
的不是函數,這時候須要拋出異常。
if (typeof this !== "function") { throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); }
因此完整版模擬實現代碼以下:
// 第五版 Function.prototype.bind2 = function (context) { if (typeof this !== "function") { throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); } var self = this; var args = Array.prototype.slice.call(arguments, 1); var fNOP = function () {}; var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); } fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }
// 一、賦值語句是右執行的,此時會先執行右側的對象 var obj = { // 二、say 是當即執行函數 say: function() { function _say() { // 五、輸出 window console.log(this); } // 三、編譯階段 obj 賦值爲 undefined console.log(obj); // 四、obj是 undefined,bind 自己是 call實現, // 【進階3-3期】:call 接收 undefined 會綁定到 window。 return _say.bind(obj); }(), }; obj.say();
call
的模擬實現以下,那有沒有什麼問題呢?
Function.prototype.call = function (context) { context = context || window; context.fn = this; var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } var result = eval('context.fn(' + args +')'); delete context.fn; return result; }
固然是有問題的,其實這裏假設 context
對象自己沒有 fn
屬性,這樣確定不行,咱們必須保證 fn
屬性的惟一性。
解決方法也很簡單,首先判斷 context
中是否存在屬性 fn
,若是存在那就隨機生成一個屬性fnxx
,而後循環查詢 context
對象中是否存在屬性 fnxx
。若是不存在則返回最終值。
一種循環方案實現代碼以下:
function fnFactory(context) { var unique_fn = "fn"; while (context.hasOwnProperty(unique_fn)) { unique_fn = "fn" + Math.random(); // 循環判斷並從新賦值 } return unique_fn; }
一種遞歸方案實現代碼以下:
function fnFactory(context) { var unique_fn = "fn" + Math.random(); if(context.hasOwnProperty(unique_fn)) { // return arguments.callee(context); ES5 開始禁止使用 return fnFactory(context); // 必須 return } else { return unique_fn; } }
模擬實現完整代碼以下:
function fnFactory(context) { var unique_fn = "fn"; while (context.hasOwnProperty(unique_fn)) { unique_fn = "fn" + Math.random(); // 循環判斷並從新賦值 } return unique_fn; } Function.prototype.call = function (context) { context = context || window; var fn = fnFactory(context); // added context[fn] = this; // changed var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } var result = eval('context[fn](' + args +')'); // changed delete context[fn]; // changed return result; } // 測試用例在下面
ES6有一個新的基本類型Symbol
,表示獨一無二的值,用法以下。
const symbol1 = Symbol(); const symbol2 = Symbol(42); const symbol3 = Symbol('foo'); console.log(typeof symbol1); // "symbol" console.log(symbol3.toString()); // "Symbol(foo)" console.log(Symbol('foo') === Symbol('foo')); // false
不能使用 new
命令,由於這是基本類型的值,否則會報錯。
new Symbol(); // TypeError: Symbol is not a constructor
模擬實現完整代碼以下:
Function.prototype.call = function (context) { context = context || window; var fn = Symbol(); // added context[fn] = this; // changed let args = [...arguments].slice(1); let result = context[fn](...args); // changed delete context[fn]; // changed return result; } // 測試用例在下面
測試用例在這裏:
// 測試用例 var value = 2; var obj = { value: 1, fn: 123 } function bar(name, age) { console.log(this.value); return { value: this.value, name: name, age: age } } bar.call(null); // 2 console.log(bar.call(obj, 'kevin', 18)); // 1 // {value: 1, name: "kevin", age: 18} console.log(obj); // {value: 1, fn: 123}
有兩種方案能夠判斷對象中是否存在某個屬性。
var obj = { a: 2 }; Object.prototype.b = function() { return "hello b"; }
in
操做符in
操做符會檢查屬性是否存在對象及其 [[Prototype]]
原型鏈中。
("a" in obj); // true ("b" in obj); // true
Object.hasOwnProperty(...)
方法hasOwnProperty(...)
只會檢查屬性是否存在對象中,不會向上檢查其原型鏈。
obj.hasOwnProperty("a"); //true obj.hasOwnProperty("b"); //false
注意如下幾點:
in
操做符能夠檢查容器內是否有某個值,實際上檢查的是某個屬性名是否存在。對於數組來講,4 in [2, 4, 6]
結果返回 false
,由於 [2, 4, 6]
這個數組中包含的屬性名是0,1,2
,沒有4
。Object.prototype
的委託來訪問 hasOwnProperty(...)
,可是對於一些特殊對象( Object.create(null)
建立)沒有鏈接到 Object.prototype
,這種狀況必須使用 Object.prototype.hasOwnProperty.call(obj, "a")
,顯示綁定到 obj
上。又是一個 call
的用法。用 JS 實現一個無限累加的函數 add
,示例以下:
add(1); // 1 add(1)(2); // 3 add(1)(2)(3); // 6 add(1)(2)(3)(4); // 10 // 以此類推
不用 call 和 apply 方法模擬實現 ES5 的 bind 方法
進階系列文章彙總以下,內有優質前端資料,以爲不錯點個star。
https://github.com/yygmind/blog
我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!