初探 this、call 和 apply

this、call 和 apply

this

JavaScript 中 this 老是指向一個對象編程

this 的指向

  1. 做爲對象的方法調用

this 指向該對象,如:設計模式

var obj = {
  a: 1,
  getA: function() {
    console.log(this === obj); // true
    console.log(this.a); // 1
  }
};

obj.getA();
複製代碼
  1. 做爲普通函數調用

當函數不做爲對象的屬性被調用時,普通函數的 this 老是指向全局對象,瀏覽器裏就是 window 對象。數組

var name = "globalName";
var obj = {
  name: "tyang",
  getName: function() {
    return this.name;
  }
};
var getName = obj.getName;
console.log(obj.getName()); // tyang
console.log(getName()); // globalName
複製代碼

obj.getName() 做爲 obj 對象的屬性被調用,this 指向 obj 對象;瀏覽器

getName() 使用變量 getName 引用 obj.getName,此時是函數調用方式,this 指向全局 window;閉包

在嚴格模式,狀況有所不一樣:this 不會指向全局對象,而是 undefined:app

function func() {
 "use strict";
  console.log(this); // undefined
}

func();
複製代碼

當函數做爲某個對象的方法調用時,this 等於那個對象。不過,匿名函數的執行環境具備全局性,所以其 this 對象一般指向 window。函數式編程

var gName = "The window";
var gObject = {
  gName: "My object",
  getName: function() {
    return function() {
      // 返回一個匿名函數
      return this.gName;
    };
  }
};

console.log(gObject.getName()()); // 'The window'
var getNameFunc = gObject.getName();
console.log(getNameFunc()); // 'The window'
複製代碼

建立了一個全局對象 gName,這個對象包含一個方法 getName(), 這個方法返回一個匿名函數,這個匿名函數返回 this.name。所以調用 gObject.getName()() 會當即執行匿名函數,並返回一個字符串 'The window'函數

爲何匿名函數沒有取得包含做用域的 this 對象呢?學習

每一個函數再被調用的時候,會自動取得兩個特殊變量:this 和 arguments,內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變量。ui

因此,能夠在外部做用域中設置一個變量來保存 this 對象:

var gName = "The window";
var gObject = {
  gName: "My object",
  getName: function() {
    var that = this; // 將 this 對象賦值給 that 變量
    return function() {
      return that.gName; // that 引用着 gObject
    };
  }
};

console.log(gObject.getName()()); // 'My object'
複製代碼

固然,arguments 對象也能夠如此使用:對該對象的引用保存到另外一個閉包可以訪問的變量中。

  1. 構造器調用

當使用 new 運算符調用函數時,該函數會返回一個對象,通常狀況下,構造器裏的 this 指向返回的這個對象,如:

var MyClass = function() {
  this.name = "Lisi";
};
var nameObj = new MyClass();
console.log(nameObj.name); // Lisi
複製代碼

可是,當顯式返回一個 object 類型的對象時,那最終會返回這個對象,並非以前的 this:

var MyClass = function() {
  this.name = "Lisi";
  return {
    // 若是這裏不會煩 object 類型的數據,如:return 'wangwu',就不會返回顯式對象
    name: "wangwu"
  };
};
var nameObj = new MyClass();
console.log(nameObj.name); // wangwu
複製代碼
  1. Function.prototype.call 或 Function.prototype.apply 調用

call 和 apply 能夠動態地改變傳入函數的 this:

var personObj = {
  name: "ytao",
  age: "22"
};
function person() {
  return this.name + this.age;
}
console.log(person.call(personObj)); // ytao22
複製代碼

丟失的 this

咱們通常會重寫這個獲取 id 的方法:

var getId = function(id) {
  return document.getElementById(id);
};

getId("divBox");
複製代碼

那可不能夠這樣呢:

getId2 = document.getElementById;
getId2("divBox"); // Uncaught TypeError: Illegal invocation
複製代碼

結果直接報錯,當 getElementById 方法做爲 document 對象的屬性被調用時, 方法內部的 this 是指向 document 的。若是 getId2('divBox'),至關因而普通函數調用,函數內部的 this 指向的是 window。

因此,按照這個思路,咱們能夠這樣模擬一下它的實現:

document.getElementById = (function(func) {
  return function() {
    return func.apply(document, arguments);
  };
})(document.getElementById);

getId3 = document.getElementById;
getId3("divBox");
複製代碼

call 和 apply

fun.apply(thisArg, [argsArray])

fun.call(thisArg, arg1, arg2, ...)

在函數式編程中,call 和 apply 方法尤其有用,二者用法一致,只是傳參的形式上有所區別而已。

區別

apply() 接受兩個參數,第一個參數指定了函數體內 this 對象,第二個是數組或者類數組,apply() 方法將這個集合中的元素做爲參數傳遞給被調用的函數。

call() 方法的做用和 apply() 方法相似,區別就是 call()方法接受的是參數列表,而 apply()方法接受的是一個參數數組。

第一個參數爲 null,函數體內的 this 會指向默認的宿主對象,可是在嚴格模式下,依然是 null。

var applyFunc = function(a, b, c) {
  console.log(this === window);
};
applyFunc.apply(null, [1, 2, 3]); // true

var applyFunc = function(a, b, c) {
 "use strict";
  console.log(this === null);
};
applyFunc.apply(null, [1, 2, 3]); // true
複製代碼

用途

  1. 改變 this 指向

假如在一個點擊事件函數中有一個內部函數 func,當點擊事件被觸發時,就會出現以下狀況:

document.getElementById("divBox").onclick = function() {
  console.log(this.id); // divBox
  var func = function() {
    console.log(this.id); // undefined,這裏的 this 指向了 window
  };
  func();
};
複製代碼

這時,咱們用 call() 來改變一下:

document.getElementById("divBox").onclick = function() {
  console.log(this.id); // divBox
  var func = function() {
    console.log(this.id); // divBox
  };
  func.call(this);
};
複製代碼
  1. 模擬 bind 方法

function.bind(thisArg[, arg1[, arg2[, ...]]])

bind()方法建立一個新的函數,在調用時設置 this 關鍵字爲提供的值。並在調用新函數時,將給定參數列表做爲原函數的參數序列的前若干項。

Function.prototype.bind = function(context) {
  var self = this; // 保存原函數
  return function() {
    // 返回新函數
    return self.apply(context, arguments); // 將傳入的 context 當作新函數體內的 this
  };
};

var bindObj = {
  name: "tyang"
};

var bindFunc = function() {
  console.log(this.name); // tyang
}.bind(bindObj);

bindFunc();
複製代碼

這是一個簡化版的 Function.prototype.bind實現,self.apply(context, arguments)纔是執行原來的 bindFunc 函數,而且指定 context 對象爲 bindFunc 函數體內的 this。

咱們再繼續修改下,使之能夠預先添加一些參數:

Function.prototype.bind = function() {
  var self = this,
    context = [].shift.call(arguments), // 獲取參數中第一個爲綁定的this上下文
    args = [].slice.call(arguments); // 將剩餘的參數轉化爲數組

  // 返回新函數
  return function() {
    return self.apply(context, [].concat.call(args, [].slice.call(arguments))); //arguments 爲新函數的參數,即傳入的 3,4
  };
};

var bindObj = {
  name: "lisisi"
};

var bindFunc = function(a, b, c, d) {
  console.log(this.name); // lisisi
  console.log([a, b, c, d]); // [1, 2, 3, 4]
}.bind(bindObj, 1, 2);

bindFunc(3, 4);
複製代碼

self.apply(context, [].concat.call(args, [].slice.call(arguments)));,執行新函數的時候,會把以前傳入的 context 做爲 this,[].slice.call(arguments)將新函數傳入的參數轉化爲數組,並做爲[].concat.call(args)的給定參數,組合兩次,做爲新函數最終的參數。

  1. 借用其餘對象的方法

第一種,」借用構造函數「實現一些相似繼承的效果:

var A = function(name) {
  this.name = name;
};
var B = function() {
  A.apply(this, arguments);
};
B.prototype.getName = function() {
  return this.name;
};
var bbb = new B("Yangtao");
console.log(bbb.getName()); //Yangtao
複製代碼

第二種,給類數組對象使用數組方法,好比:

(function() {
  Array.prototype.push.call(arguments, 3);
  console.log(arguments); // [1, 2, 3]
})(1, 2);
複製代碼

再好比以前用到的,把 arguments 轉成真正的數組的時候能夠借用 Array.prototype.slice.call(arguments),想截去頭一個元素時,借用Array.prototype.shift.call(arguments)

雖然咱們能夠把」任意「對象傳入 Array.prototype.push:

var aObj = {};
Array.prototype.push.call(aObj, "first");
console.log(aObj.length); // 1
console.log(aObj[0]); // first
複製代碼

可是,這個對象也得知足如下兩個條件:

  • 對象自己要能夠存取屬性
  • 對象的 length 屬性可讀寫

若是是其餘類型,好比 number,沒法存取;好比函數,length 屬性不可寫,使用 call 或 apply 就會報錯:

var num = 1;
Array.prototype.push.call(num, "2");
console.log(num.length); // undefined
console.log(num[0]); // undefined

var funcObj = function() {};
Array.prototype.push.call(funcObj, "3");
console.log(funcObj.length); // Uncaught TypeError: Cannot assign to read only property 'length' of function 'function () {}'
複製代碼

學習資料:

  • 《JavaScript 高程 3》第七章
  • 《JavaScript 設計模式與開發實踐 · 曾探》第 2 章
相關文章
相關標籤/搜索