bind()
bind()
方法會創建一個新函數,當這個新函數被調用時,它的this
值是傳遞給bind()
的第一個參數,傳入bind方法的第二個以及以後的參數加上綁定函數運行時本身的參數按照順序作爲原函數的參數來調用原函數。bind返回的綁定函數也能使用new
操作符創建對象:這種行爲就像把原函數當成構造器,提供的this
值被忽略,同時調用時的參數被提供給模擬函數。(來自參考1)
語法:fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind
方法與 call / apply
最大的不同就是前者返回一個綁定上下文的函數,而後兩者是直接執行了函數。
來個例子說明下
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
有如下特性:
- 1、可以指定
this
- 2、返回一個函數
- 3、可以傳入參數
- 4、柯里化
#使用場景
#1、業務場景
經常有如下的業務場景
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
。
這邊把 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
完美!
#2、驗證是否是數組
【進階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
#3、柯里化(curry)
只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
可以一次性地調用柯里化函數,也可以每次只傳一個參數分多次調用。
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 來實現。
首先我們來實現以下四點特性:
- 1、可以指定
this
- 2、返回一個函數
- 3、可以傳入參數
- 4、柯里化
#模擬實現第一步
對於第 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 指向實例,此時
this instanceof fBound
結果爲true
,可以讓實例獲得來自綁定函數的值,即上例中實例會具有habit
屬性。 - 當作爲普通函數時,this 指向
window
,此時結果爲false
,將綁定函數的 this 指向context
- 當作爲構造函數時,this 指向實例,此時
- 註釋2: 修改返回函數的
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;
}
#【進階3-2期】思考題解
// 1、賦值語句是右執行的,此時會先執行右側的對象
var obj = {
// 2、say 是立即執行函數
say: function() {
function _say() {
// 5、輸出 window
console.log(this);
}
// 3、編譯階段 obj 賦值爲 undefined
console.log(obj);
// 4、obj是 undefined,bind 本身是 call實現,
// 【進階3-3期】:call 接收 undefined 會綁定到 window。
return _say.bind(obj);
}(),
};
obj.say();
#【進階3-3期】思考題解
call
的模擬實現如下,那有沒有什麼問題呢?
Function.prototype.call = function (context) {
context = context ? Object(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
屬性的唯一性。
#ES3下模擬實現
解決方法也很簡單,首先判斷 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 ? Object(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下模擬實現
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 ? Object(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";
}
- 1、
in
操作符
in
操作符會檢查屬性是否存在對象及其 [[Prototype]]
原型鏈中。
("a" in obj); // true
("b" in obj); // true
- 2、
Object.hasOwnProperty(...)
方法
hasOwnProperty(...)
只會檢查屬性是否存在對象中,不會向上檢查其原型鏈。
obj.hasOwnProperty("a"); //true
obj.hasOwnProperty("b"); //false
注意以下幾點:
- 1、看起來
in
操作符可以檢查容器內是否有某個值,實際上檢查的是某個屬性名是否存在。對於數組來說,4 in [2, 4, 6]
結果返回false
,因爲[2, 4, 6]
這個數組中包含的屬性名是0,1,2
,沒有4
。 - 2、所有普通對象都可以通過
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
// 以此類推