改變函數執行時的上下文(改變函數運行時的this指向)javascript
// 寫一個構造函數類
function Person(name){
this.name = name;
}
Person.prototype = {
constructor: Person,
showName: function(){
console.log(this.name);
}
}
// 用該類new出的新對象擁有此類的屬性和方法
var person = new Person('wuqinhao');
person.showName();
複製代碼
而後咱們遇到個小需求,有一個對象不是基於此類new出,但想要showName方法,改如何處理呢java
// 這個對象只有一個name屬性,此時它想擁有一個showName方法
var animal = {
name: 'cat'
}
// 雖然它能夠再寫一遍showName方法,可從代碼設計角度來講不太優雅,可重用的方法寫了兩遍。
// 因而創始者發明了call、apply、bind三個方法來解決此問題
// 1 call
person.showName.call(animal);
// 2 apply
person.showName.apply(animal);
// 3 bind
person.showName.bind(animal)();
複製代碼
咱們發現apply和call差很少,差異是一個傳數組,一個傳多個參數。這是創始者爲了開發者在不一樣的語境方便調用不一樣的方法。(就好比你設計一個類庫時可能也會暴露兩個不一樣的API,方便開發者調用)git
// 1.使用 call 方法調用父構造函數
// 通常用call,能夠明確的看到傳遞的參數
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
function Toy(name, price) {
Product.call(this, name, price);
this.category = 'toy';
}
var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);
console.log(cheese);
console.log(fun);
複製代碼
// 1.數組合並
// 通常用apply,傳遞數組方便
var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
[].push.apply(arr1, arr2);
console.log(arr1);
console.log(arr2);
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]
複製代碼
// 2.調用封裝好的內置函數
/* 找出數組中最大/小的數字 */
var numbers = [5, 6, 2, 3, 7];
/* 應用(apply) Math.min/Math.max 內置函數完成 */
var max = Math.max.apply(null, numbers); /* 基本等同於 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);
// 若是咱們不用apply,那隻能用通常的for循環逐一查找
var max = -Infinity;
var min = +Infinity;
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] > max) { max = numbers[i]; }
if (numbers[i] < min) { min = numbers[i]; }
}
複製代碼
// 1.配合 setTimeout 綁定this到當前實例,而不是window
function Person(name, age){
this.name = name;
this.age = age
}
Person.prototype = {
constructor: Person,
showName: function(){
console.log(this.name);
},
showAge: function(){
setTimeout(function () {
console.log(this.age)
}.bind(this), 1000)
// setTimeout(function () {
// console.log(this.age)
// }, 1000)
// 若是不bind,這裏的setTimeOut是window下面的方法,因此this指向會指到window,而window下沒有age,因此會輸出undefined
// setTimeout( () => {
// console.log(this.age)
// }, 1000)
// 因爲es6裏出現的箭頭函數,他能將this指向當前調用的實例上,因此用此方法也是可行的
}
}
// 用該類new出的新對象擁有此類的屬性和方法
var person = new Person('wuqinhao', 26);
person.showAge();
// 一秒中後打印 26
複製代碼
// 2.偏函數(使一個函數擁有預設的初始參數)
function addArguments(arg1, arg2) {
return arg1 + arg2
}
var result1 = addArguments(1, 2); // 3
// 建立一個函數,它擁有預設的第一個參數
var addThirtySeven = addArguments.bind(null, 37);
var result2 = addThirtySeven(5);
// 37 + 5 = 42
var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二個參數被忽略
/* 只要將這些參數(若是有的話)做爲bind()的參數寫在this後面。 當綁定函數被調用時,這些參數會被插入到目標函數的參數列表的開始位置, 傳遞給綁定函數的參數會跟在它們後面。*/
複製代碼
咱們看前面的引子示例,animal想執行showName方法,但又不想從新寫一遍就調用了call方法。若是咱們寫一遍showName方法,執行完,而後再刪除這個方法,而把這個過程封裝成一個函數,這個函數不就有點像call嘛。es6
因此咱們模擬的步驟能夠分爲:github
// 初版
Function.prototype.call2 = function(context) {
// 首先要獲取調用call的函數,用this能夠獲取
context.fn = this;
context.fn();
// fn這個名字能夠隨便取,由於後面會delete掉
delete context.fn;
}
// 測試一下(此代碼的前提是要有引子裏的代碼)
person.showName.call2(animal); // cat
複製代碼
注意:傳入的參數並不肯定,咱們能夠從 Arguments 對象中取值,取出第二個到最後一個參數,而後放到一個數組裏。數組
// 第二版
Function.prototype.call2 = function(context) {
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
console.log(args); // ["arguments[1]", "arguments[2]"]
eval('context.fn(' + args +')'); // 至關於eval('context.fn(arguments[1],arguments[2])')
// args會自動調用 Array.toString()
delete context.fn;
}
// 測試一下
// 1.使用 call 方法調用父構造函數
// 通常用call,能夠明確的看到傳遞的參數
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call2(this, name, price);
this.category = 'food';
}
var cheese = new Food('feta', 5);
console.log(cheese);
複製代碼
eval語法請看 developer.mozilla.org/zh-CN/docs/… Arguments語法請看 developer.mozilla.org/zh-CN/docs/…app
1.this 參數能夠傳 null,當爲 null 的時候,視爲指向 window(當不綁定this時,能夠指向window)函數
2.函數是能夠有返回值(retrun 出eval的結果便可)測試
// 第三版
Function.prototype.call2 = function(context) {
context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
console.log(args); // ["arguments[1]", "arguments[2]"]
var result = eval('context.fn(' + args +')'); // 至關於eval('context.fn(arguments[1],arguments[2])')
// args會自動調用 Array.toString()
delete context.fn;
return result;
}
// 測試一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.call2(null); // 2
console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
複製代碼
apply的實現與call類型,區別在與apply傳入數組。因此arguments獲取改爲arr數組。優化
Function.prototype.apply2 = function(context, arr) {
context = context || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn()
} else {
var args = [];
for(var i = 1, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
console.log(args); // ["arr[1]", "arr[2]"]
result = eval('context.fn(' + args +')'); // 至關於eval('context.fn(arr[1],arr[2])')
// args會自動調用 Array.toString()
}
delete context.fn;
return result;
}
複製代碼
1.返回一個函數。2.能夠傳入參數。3.一個綁定函數也能使用new操做符建立對象。
Function.prototype.bind2 = function (context) {
var self = this;
return function () {
// 應用apply來指定this的指向
self.apply(context);
}
}
複製代碼
有時綁定的函數可能也會有返回值,因此綁定的函數return了一個內容,若是再也不加個return,內容是返回不出來的。
Function.prototype.bind2 = function (context) {
var self = this;
return function () {
// 應用apply來指定this的指向
return self.apply(context); // 再加一個return,將綁定函數的返回值return出來。
}
}
複製代碼
先看一個例子(傳參還能在返回的函數裏繼續傳)
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy', '18');
bindFoo()
// 1
// daisy
// 18
var bindFoo1 = bar.bind(foo, 'daisy');
bindFoo1('18');
// 1
// daisy
// 18
複製代碼
解決辦法:仍是使用arguments,只不過在最後將參數合併到一塊兒再使用
Function.prototype.bind2 = function (context) {
var self = this;
// 獲取bind2函數從第二個參數到最後一個參數
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 這個時候的arguments是指bind返回的函數傳入的參數
var bindArgs = Array.prototype.slice.call(arguments);
// 應用apply來指定this的指向
return self.apply(context, args.concat(bindArgs)); // 再加一個return,將綁定函數的返回值return出來。
}
}
複製代碼
看個例子
var value = 2;
var foo = {
value: 1
};
function bar (name, age) {
this.a = 'aaa';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.b = 'bbb';
var bindFoo = bar.bind(foo, 'wqh');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.a);
console.log(obj.b);
// shopping
// kevin
複製代碼
咱們看到現象:當 bind 返回的函數做爲構造函數的時候,bind 時指定的 this 值會失效,但傳入的參數依然生效。this指向了obj,然而obj上沒有value屬性,因此是undefined
實現
Function.prototype.bind2 = function (context) {
var self = this;
// 獲取bind2函數從第二個參數到最後一個參數
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 看成爲構造函數時,this指向實例,此時結果爲true,將綁定函數的this指向該實例,可讓實例得到來自綁定函數的值
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
// 看成爲普通函數時,this指向window,此時結果未false,將綁定函數的this指向context
// 以上代碼若是改爲`this instanceof fBound ? null : context`實例只是一個空對象,將null改成this,實例會具備綁定函數的屬性
}
fBound.prototype = this.prototype;
return fBound;
}
複製代碼
優化一波
1.在這個寫法中,咱們直接將 fBound.prototype = this.prototype,咱們直接修改 fBound.prototype 的時候,也會直接修改綁定函數的 prototype。這個時候,咱們能夠經過一個空函數來進行中轉 2.當調用bind的不是函數時,咱們要拋出異常
Function.prototype.bind2 = function (context) {
if (typeof this !== 'function') {
throw new Error('Function.prototype.bind - 試圖綁定的內容不可調用');
}
var self = this;
// 獲取bind2函數從第二個參數到最後一個參數
var args = Array.prototype.slice.call(arguments, 1);
var f = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 看成爲構造函數時,this指向實例,此時結果爲true,將綁定函數的this指向該實例,可讓實例得到來自綁定函數的值
return self.apply(this instanceof f ? this : context, args.concat(bindArgs));
// 看成爲普通函數時,this指向window,此時結果未false,將綁定函數的this指向context
// 以上代碼若是改爲`this instanceof f ? null : context`實例只是一個空對象,將null改成this,實例會具備綁定函數的屬性
}
f.prototype = this.prototype;
fBound.prototype = new f();
return fBound;
}
複製代碼