導讀
作為面試中面試官最寵愛的一個問題,在這裡進行一個詳細的介紹,大家重點要放在理解,而不是背。 寫的不好或不對的地方,請大家積極指出,好了,話不多說,我們“圓規正轉”
先說一下三者的區別
共同點就是修改this指向,不同點就是
1.call()和apply()是立刻執行的, 而bind()是返回了一個函式
2.call則可以傳遞多個引數,第一個引數和apply一樣,是用來替換的物件,後邊是引數列表。
3.apply最多隻能有兩個引數——新this物件和一個陣列argArray
複製程式碼
一、手寫實現Call
1.call主要都做了些什麼。
- 更改this指向
- 函式立刻執行
2.簡單實現
Function.prototype.myCall = function(context) {
context.fn = this;
context.fn();
}
const obj = {
value: 'hdove'
}
function fn() {
console.log(this.value);
}
fn.myCall(obj); // hdove
複製程式碼
3.出現的問題
- 無法傳值
- 如果fn()有返回值的話,myCall 之後獲取不到
function fn() {
return this.value;
}
console.log(fn.myCall(obj)); // undefined
複製程式碼
- call其實就是更改this指向,指向一個Object,如果使用者傳的是基本型別又或者乾脆就不傳呢?
- myCall執行之後,obj會一直綁著fn()
4.統統解決
Function.prototype.myCall = function(context) {
// 1.判斷有沒有傳入要繫結的物件,沒有預設是window,如果是基本型別的話通過Object()方法進行轉換(解決問題3)
var context = Object(context) || window;
/**
在指向的物件obj上新建一個fn屬性,值為this,也就是fn()
相當於obj變成了
{
value: 'hdove',
fn: function fn() {
console.log(this.value);
}
}
*/
context.fn = this;
// 2.儲存返回值
let result = '';
// 3.取出傳遞的引數 第一個引數是this, 下面是三種擷取除第一個引數之外剩餘引數的方法(解決問題1)
const args = [...arguments].slice(1);
//const args = Array.prototype.slice.call(arguments, 1);
//const args = Array.from(arguments).slice(1);
// 4.執行這個方法,並傳入引數 ...是es6的語法用來展開陣列
result = context.fn(...args);
//5.刪除該屬性(解決問題4)
delete context.fn;
//6.返回 (解決問題2)
return result;
}
const obj = {
value: 'hdove'
}
function fn(name, age) {
return {
value: this.value,
name,
age
}
}
fn.myCall(obj, 'LJ', 25); // {value: "hdove", name: "LJ", age: 25}
複製程式碼
二、手動實現Apply
實現了call其實也就間接實現了apply,只不過就是傳遞的引數不同
Function.prototype.myApply = function(context, args) {
var context = Object(context) || window;
context.fn = this;
let result = '';
//4. 判斷有沒有傳入args
if(!args) {
result = context.fn();
}else {
result = context.fn(...args);
}
delete context.fn;
return result;
}
const obj = {
value: 'hdove'
}
function fn(name, age) {
return {
value: this.value,
name,
age
}
}
fn.myApply(obj, ['LJ', 25]); // {value: "hdove", name: "LJ", age: 25}
複製程式碼
三、實現Bind
bind() 方法建立一個新的函式,在 bind() 被呼叫時,這個新函式的 this 被指定為 bind() 的第一個引數,而其餘引數將作為新函式的引數,供呼叫時使用(MDN)
複製程式碼
1.bind特性
- 指定this
- 返回一個函式
- 傳遞引數並柯里化
2.簡單實現
Function.prototype.myBind = function(context) {
const self = this;
return function() {
self.apply(context);
}
}
const obj = {
value: 'hdove'
}
function fn() {
console.log(this.value);
}
var bindFn = fn.myBind(obj);
bindFn(); // 'hdove;
複製程式碼
3.優化
相比於call、apply,我個人覺得bind的實現邏輯更加複雜,需要考慮的東西很多,在這裡分開進行優化。
3.1 呼叫bind是個啥玩意?
在這裡我們需要進行一下判斷,判斷呼叫bind
的是不是一個函式,不是的話就要丟擲錯誤。
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new Error("不是一個函式");
}
const self = this;
return function() {
self.apply(context);
}
}
複製程式碼
3.2 傳遞引數
我們看下面這段程式碼
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new Error("不是一個函式");
}
const self = this;
return function() {
self.apply(context);
}
}
const obj = {
value: 'hdove'
}
function fn(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFn = fn.myBind(obj, LJ, 25);
bindFn(); // 'hdove' undefined undefined
複製程式碼
很明顯,第一個優化的地方就是傳遞引數,我們來改造下
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new Error("不是一個函式");
}
const self = this;
// 第一個引數是this,擷取掉
const args = [...arguments].slice(1);
return function() {
/**
這裡我們其實即可以使用apply又可以使用call來更改this的指向
使用apply的目的其實就是因為args是一個陣列,更符合apply的條件
*/
return self.apply(context, args);
}
}
const obj = {
value: 'hdove'
}
function fn(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFn = fn.myBind(obj, 'LJ', 25);
bindFn(); // 'hdove' 'LJ' 25
複製程式碼
想在看起來沒什麼問題,但是我們這樣傳一下引數
var bindFn = fn.myBind(obj, 'LJ');
bindFn(25); // 'hdove' 'LJ' undefined
複製程式碼
我們發現後面傳遞的引數丟了,這裡就需要使用柯里化來解決這個問題
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new Error("不是一個函式");
}
const self = this;
// 第一個引數是this,擷取掉
const args1 = [...arguments].slice(1);
return function() {
// 獲取呼叫時傳入的引數
const args2 = [...arguments];
return self.apply(context, args1.concat(args2));
}
}
const obj = {
value: 'hdove'
}
function fn(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFn = fn.myBind(obj, 'LJ');
bindFn(25); // 'hdove' 'LJ' 25
複製程式碼
3.3this丟失
其實bind
還具有一個特性就是 作為建構函式使用的繫結函式
,意思就是這個繫結函式可以當成建構函式使用,可以呼叫new
操作符去建立一個例項,當我們使用new
操作符之後,this
其實不是指向我們指定的物件,而是指向new
出來的這個例項的建構函式,不過提供的引數列表仍然會插入到建構函式呼叫時的引數列表之前。我們簡單實現一下。
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new Error("不是一個函式");
}
const self = this;
const args1 = [...arguments].slice(1);
const bindFn = function() {
const args2 = [...arguments];
/**
這裡我們通過列印this,我們可以看出來。
當這個繫結函式被當做普通函式呼叫的時候,this其實是指向window。
而當做建構函式使用的時候,卻是指向這個例項,所以this instanceof bindFn為true,這個例項可以獲取到fn()裡面的值。
我們可以再fn裡面新增一個屬性test.
如果按照之前的寫法 列印出來的是undefined,正好驗證了我們上面所說的this指向的問題。
所以解決方法就是新增驗證,判斷當前this
如果 this instanceof bindFn 說明這是new出來的例項,指向這個例項, 否則指向context
*/
console.log(this);
return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
}
return bindFn;
}
const obj = {
value: 'hdove'
}
function fn(name, age) {
this.test = '我是測試資料';
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFn = fn.myBind(obj, 'LJ');
var newBind = new bindFn(25);
console.log(newBind.test); // undefined
複製程式碼
3.4繫結原型
我們都知道每一個建構函式,都會有一個原型物件(prototype),來新增額外的屬性。
function fn(name, age) {
this.test = '我是測試資料';
}
fn.prototype.pro = '原型資料';
var bindFn = fn.myBind(obj, 'LJ', 25);
var newBind = new bindFn();
console.log(bindObj.pro); // undefined
複製程式碼
因為我們沒有繫結原型,所以會出現undefined
,我們簡單繫結一下
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new Error("不是一個函式");
}
const self = this;
const args1 = [...arguments].slice(1);
const bindFn = function() {
const args2 = [...arguments];
return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
}
// 繫結原型
bindFn.prototype = self.prototype;
return bindFn;
}
function fn(name, age) {
this.test = '我是測試資料';
}
fn.prototype.pro = '原型資料';
var bindFn = fn.myBind(obj, 'LJ', 25);
var newBind = new bindFn();
console.log(bindObj.pro); // "原型資料"
複製程式碼
但是這樣會出現這樣一個問題
function fn(name, age) {
this.test = '我是測試資料';
}
fn.prototype.pro = '原型資料';
var bindFn = fn.myBind(obj, 'LJ');
var bindObj = new bindFn();
bindObj.__proto__.pro = '篡改原型資料';
console.log(bindObj.__proto__ === fn.prototype); // true
console.log(bindObj.pro); // "篡改原型資料"
console.log(fn.prototype.pro); // "篡改原型資料"
當我們修改bindObj的原型的時候,fn的原型也一起修改了
這其實是因為 bindObj.__proto__ === fn.prototype
我們在修改bindObj的同時也間接修改了fn
複製程式碼
解決方法其實很簡單,建立一個新方法proFn()
,來進行原型繫結,也就是實現繼承的幾種方式中的原型式繼承,然後我們把這個新方法的例項物件繫結到我們的繫結函式的原型中
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new Error("不是一個函式");
}
const self = this;
const args1 = [...arguments].slice(1);
const bindFn = function() {
const args2 = [...arguments];
return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
}
// 繫結原型
function proFn() {} //建立新方法
proFn.prototype = self.prototype; //繼承原型
bindFn.prototype = new proFn(); //繫結原型
return bindFn;
}
function fn(name, age) {
this.test = '我是測試資料';
}
fn.prototype.pro = '原型資料';
var bindFn = fn.myBind(obj, 'LJ', 25);
var newBind = new bindFn();
console.log(bindObj.__proto__ === fn.prototype); // false
console.log(bindObj.pro); // "篡改原型資料"
console.log(fn.prototype.pro); // "原型資料"
複製程式碼
四、面試
這些東西其實是面試中比較容易考到的問題,大家不要想著去背,背下來其實是沒什麼用處的,容易會被問倒,重點還是在於理解,理解了也就可以輕而易舉的寫出來了。希望這篇文章會給大家帶來收穫,那怕是一點點。在這裡,提前給大傢伙拜個早年,鼠年幸運,跳槽順利,漲薪順利,哈哈。
5.推薦閱讀
- 再回家之前,先new個物件吧
- 仿網易雲音樂webApp
- 最近在研究Taro,然後基於Taro + TS + Hook + MongoDB + KeystoneJS 寫了一個影像識別的小玩意,歡迎圍觀