JavaScript中call,apply,bind的區別與實現

區別

  • call、apply、bind相同點:都是改變this的指向,傳入的第一個參數都是綁定this的指向,在非嚴格模式中,如果第一個參數是nul或者undefined,會把全局對象(瀏覽器是window)作為this的值,要註意的是,在嚴格模式中,null 就是 null,undefined 就是 undefined
  • call和apply唯一的區別是:call傳入的是參數列表,apply傳入的是數組,也可以是類數組
  • bind和call、apply的區別: bind返回的是一個改變瞭this指向的函數,便於稍後調用,不像call和apply會立即調用;bind和call很像,傳入的也是參數列表,但是可以多次傳入,不需要像call,一次傳入
  • 值得註意:當 bind 返回的函數 使用new作為構造函數時,綁定的 this 值會失效,this指向實例對象,但傳入的參數依然生效 (new調用的優先級 > bind調用)

call實現

對象context想調用一個它沒有的方法f 怎麼辦呢?f.call(context) 通過call來借用方法f ,怎麼做到的呢?

  • 對象context添加f方法
  • 對象context執行f方法
Function.prototype.myCall = function(context, ...arg) {
    // 如果第一個參數傳入的是undefined和null,context為window對象
    context = context || window;  
    
    // 為context對象添加函數bar
    context.fn = this;   // this:bar,this指向調用myCall的bar  

    // context對象執行函數bar,並返回結果
    return  context.fn(...arg); 
}

// 測試一下
var value = 2;

var obj = {
    value: 1
}
function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.myCall(null); // 2

console.log(bar.myCall(obj, 'kevin', 18)); //1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

apply實現

apply和call唯一的區別是:call傳入的是參數列表,apply傳入的是數組,也可以是類數組

Function.prototype.myApply = function(context, arg) {
    // 如果第一個參數傳入的是undefined和null,context為window對象
    context = context || window;  
    
    // context對象添加函數bar
    context.fn = this;   // this:bar,this指向調用myCall的函數bar  

    // context對象執行函數bar,並返回結果
    let result = null;
    if (!arg) { // 沒有傳入數組
        result = context.fn();
    }else{      // 傳入瞭參數數組
        result = context.fn(...arg);
    } 
    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.myApply(null); // 2
console.log(bar.myApply(obj, ['kevin', 18])); // 1

bind實現

  • bind和call、apply的區別: bind返回的是一個改變瞭this指向的函數,便於稍後調用,不像call和apply會立即調用;bind和call很像,傳入的也是參數列表,但是可以多次傳入,不需要像call,一次傳入
  • 值得註意:當 bind 返回的函數 使用new作為構造函數時,綁定的 this 值會失效,this指向實例對象,但傳入的參數依然生效 (new調用的優先級 > bind調用)

bind實現最為復雜,因為經過bind綁定過的函數,既可以被當作普通函數調用,又可以被當作構造函數調用

  • bind 返回的函數 作為普通函數調用
// bind 返回的函數 作為普通函數調用
let bindFun = normalFun.myBind(obj, '我是參數傳進來的name') // this:obj
bindFun('我是參數傳進來的age')
  • bind 返回的函數 作為構造函數調用,綁定的 this 值obj會失效,this指向實例對象a
// bind 返回的函數 作為構造函數調用
let bindFun = Person.myBind(obj, '我是參數傳進來的name') // this:obj
let a = new bindFun('我是參數傳進來的age')               // this:a

bind 返回的函數 作為普通函數調用 代碼實現

// bind 返回的函數 作為普通函數調用
Function.prototype.myBind = function (context, ...args){
    // 如果第一個參數傳入的是undefined和null,context為window對象
    context = context || window;

    // context對象(obj)添加函數normalFun
    context.fn = this;  // this:normalFun, context.fn === normalFun,下面出現context.fn都可以直接看成normalFun
    
    // bind返回的函數  
    return function (...innerArgs) {
        // bind 返回的函數 作為普通函數被執行
        context.fn(...[...args,...innerArgs]);  //相當於normalFun(...[...args,...innerArgs])
    }
}

// 測試
let obj = {
  objName: '我是obj傳進來的name',
  objAge: '我是obj傳進來的age'
}
// 普通函數
function normalFun(name, age) {
  console.log(name);          //'我是第一次參數傳進來的name被args接收'
  console.log(age);           //'我是第二次參數傳進來的age被innerArgs接收'
  console.log(this === obj);  // true,this指向obj
  console.log(this.objName);  //'我是obj傳進來的name'
  console.log(this.objAge);   //'我是obj傳進來的age'
}

// bind 返回的函數 作為普通函數調用
let bindFun = normalFun.myBind(obj, '我是第一次參數傳進來的name被args接收'); // this指向obj 
bindFun('我是第二次參數傳進來的age被innerArgs接收'); 

bind 返回的函數 作為構造函數調用

// bind 返回的函數 再經過new調用  
Function.prototype.myBind = function (context, ...args){
    // 如果第一個參數傳入的是undefined和null,context為window對象
    context = context || window;

    // context對象添加函數Person
    context.fn = this;     // this:Person,context.fn:Person,_this:Person
    let _this = this;

    // bind返回的函數 
    const result = function (...innerArgs) { 
        if (this instanceof _this ) { // this:a (new出來的實例對象) ,  _this:Person
            // 為實例對象a添加Person方法
            this.fn = _this;
            // 實例對象a執行Person方法
            this.fn(...[...args,...innerArgs]);
        }
    }
    result.prototype = Object.create(this.prototype);  // 為什加這一句?看原型圖下面會解釋
    return result;
}
// 測試
function Person(name, age) {
  console.log(name); //'我是第一次參數傳進來的name被args接收'
  console.log(age); //'我是第二次參數傳進來的age被innerArgs接收'
  console.log(this); //構造函數this指向實例對象
}
// 構造函數原型的方法
Person.prototype.say = function() {
  console.log(123);
}

let obj = {
  objName: '我是obj傳進來的name',
  objAge: '我是obj傳進來的age'
}

// bind 返回的函數 作為構造函數調用
let bindFun = Person.myBind(obj, '我是第一次參數傳進來的name被args接收') // this:obj
let a = new bindFun('我是第二次參數傳進來的age被innerArgs接收')               // this:a
a.say() //123

畫以下兩條語句的原型圖方便理解

let bindFun = Person.myBind(obj, '我是第一次參數傳進來的name被args接收') // this:obj
let a = new bindFun('我是第二次參數傳進來的age被innerArgs接收')               // this:a

當執行下面語句時,原型圖如下:

let bindFun = Person.myBind(obj, '我是第一次參數傳進來的name被args接收') // this:obj

當執行下面語句時,bindFun就是result看代碼,原型圖如下:

let a = new bindFun('我是第二次參數傳進來的age被innerArgs接收')               // this:a

在這裡實例對象a還需要繼承構造函數Person的原型,所以加上瞭

result.prototype = Object.create(this.prototype);

原型圖最終如下:

bind代碼最終實現

Function.prototype.myBind = function (context, ...args){
    // 如果第一個參數傳入的是undefined和null,context為window對象
    context = context || window;

    // context對象添加函數Person
    context.fn = this;     // this:Person,context.fn:Person,_this:Person
    let _this = this;
    
    // bind返回的函數 
    const result = function (...innerArgs) { 
        if (this instanceof _this ) { // this:a (new出來的實例對象) ,  _this:Person
            // 為實例對象a添加Person方法
            this.fn = _this;
            // 實例對象a執行Person方法
            this.fn(...[...args,...innerArgs]);
        }else{
            // 普通函數被調用
            context.fn(...[...args,...innerArgs]);
        }
    }
    
    result.prototype = Object.create(this.prototype);  // 為什加這一句?看原型圖下面會解釋
    return result;
}

// 測試
// function Person(name, age) {
//   console.log(name); //'我是第一次參數傳進來的name被args接收'
//   console.log(age); //'我是第二次參數傳進來的age被innerArgs接收'
//   console.log(this); //構造函數this指向實例對象
// }
// // 構造函數原型的方法
// Person.prototype.say = function() {
//   console.log(123);
// }

// let obj = {
//   objName: '我是obj傳進來的name',
//   objAge: '我是obj傳進來的age'
// }

// // bind 返回的函數 作為構造函數調用
// let bindFun = Person.myBind(obj, '我是第一次參數傳進來的name被args接收') // this:obj
// let a = new bindFun('我是第二次參數傳進來的age被innerArgs接收')               // this:a
// a.say() //123

// 測試
let obj = {
  objName: '我是obj傳進來的name',
  objAge: '我是obj傳進來的age'
}

// 普通函數
function normalFun(name, age) {
  console.log(name);          //'我是第一次參數傳進來的name'
  console.log(age);           //'我是第二次參數傳進來的age'
  console.log(this === obj);  // true
  console.log(this.objName);  //'我是obj傳進來的name'
  console.log(this.objAge);   //'我是obj傳進來的age'
}

// bind 返回的函數 作為普通函數調用
let bindFun = normalFun.myBind(obj, '我是第一次參數傳進來的name被args接收'); // this指向obj 
bindFun('我是第二次參數傳進來的age被innerArgs接收');  

到此這篇關於JavaScript中call,apply,bind的區別與實現的文章就介紹到這瞭,更多相關JS call,apply,bind區別內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: