在 JavaScript 中並無 OOP 編程的概念,咱們談到的 this 不一樣於一些 OOP 編程裏指向的實例化對象,它指的是運行時的上下文。所謂上下文,就是運行時所在的環境對象,好比你在公司,可能你的領導是你的部門經理,在家就是你媳婦兒同樣,不一樣的場合上下文是不同的。編程
在 JavaScript 中函數具備定義時上下文、運行時上下文以及上下文可改變的特色,也就是說函數中的 this 在不一樣的場合對應不一樣的值。在變量對象與做用域鏈一文中咱們談到 this 的肯定是在執行環境的建立階段完成的,也就是說 this 在運行時是基於執行環境綁定的,在全局執行函數,this 就指向全局(瀏覽器中爲 window),若是函數做爲一個對象的方法調用時,this 就指向這個對象。數組
全局調用瀏覽器
來看下面的的例子。bash
例 1:閉包
以下,函數 getName 在全局調用,this 指向全局對象app
var name = 'lily';
function getName() {
var name = 'lucy';
console.log('name:', this.name);
};
//非嚴格模式下等同於window.getName()
getName();
=> name: lily
複製代碼
例 2:函數
以下,儘管函數 getNameFunc 爲 boy 對象的方法,但因其在全局調用,this 一樣指向全局對象。post
var name = 'lily';
var boy = {
name: 'lucy',
getName: function() {
console.log('name:', this.name);
},
};
var getNameFunc = boy.getName;
getNameFunc();
=> name: lily
複製代碼
例 3:this
以下,boy.getName()返回一個匿名函數,假如這個匿名函數叫作 f,則(boy.getName())()
等同於f()
,等同於在全局中調用,所以 this 一樣指向全局對象。spa
var name = 'lily';
var boy = {
name: 'lucy',
getName: function() {
var name = 'snow';
return function() {
console.log('name:', this.name);
}
},
};
(boy.getName())();
=> name: lily
複製代碼
爲了保持 this 能夠經過閉包實現,以下,執行 boy.getName()時,this 指向當前執行環境 boy,所以 that 指向 boy,屬性 name 爲 lily,匿名函數執行console.log('name:', that.name);
時,因爲做用域鏈的關係,能夠訪問到上級做用域的 that 對象,指向 boy,所以 that.name 爲 lucy
var name = 'lily';
var boy = {
name: 'lucy',
getName: function() {
var name = 'snow';
var that = this;
return function() {
console.log('name:', that.name);
}
}
};
(boy.getName())();
=> name: lucy
複製代碼
對象調用
以下,對象 boy 調用本身的方法 getName,this 則指向 boy。
var boy = {
name: 'lucy',
getName: function() {
console.log('name:', this.name);
}
};
boy.getName();
=> name: lucy
複製代碼
構造函數調用
想要知道調用構造函數 this 如何指向,須要知道 new 操做符究竟作了什麼。 以下爲 new 的模擬實現:
function mockNew(f) {
// 1.
var newObj, returnObj, proto;
// 2.
proto = Object(f.prototype) === f.prototype ? f.prototype : Object.prototype;
// 3.
newObj = Object.create(proto);
// 4.
/*
arguments爲類數組對象須要經過Array.prototype.slice.call將其轉換爲數組;
Array.prototype.slice.call(arguments, 1)中的1是爲了去掉arguments的第一個參數(函數f),而沒有從0開始;
經過f.apply調用,則將this指向了newObj。
*/
returnObj = f.apply(newObj, Array.prototype.slice.call(arguments, 1));
// 5.
// 檢查returnObj是否爲Object類型
if (Object(returnObj) === returnObj) {
return returnObj;
}
return newObj;
}
複製代碼
經過 new 操做符調用構造函數(利用內置[[Construct]]
方法),會經歷如下幾個階段:
給內置屬性[[prototype]]
(proto)賦值
若是 f 的prototype爲原始的 Object 類型,則將構造函數 f 的 prototype 賦值給 proto,不然將 Object 的 prototype 賦值給 proto。
建立繼承自 proto 的對象
經過 Object.create 建立對象 newObj,newObj 可經過原型鏈繼承 proto 的屬性。
綁定 this,將其值設置爲第三步生成的對象
4.1. 經過 f.apply 調用 f,等同於 newObj.f(), 在構造函數 f 中執行 this.xxx = xxx;等同於執行 newObj.xxx = xxx,至關於 this 綁定了 newObj。
4.2. 調用構造函數,可能返回一個對象 returnObj。
返回新生成的對象
若是第 4 步中的 returnObj 值爲 Object 類型,則 new 操做最終返回這個對象 returnObj,不然返回第 4 步中綁定this的的 newObj。
來看下面的例子:
經過 mockNew 函數構造對象的過程當中,會調用上述第 4 步 f.apply(newObj, Array.prototype.slice.call(arguments, 1)),等同於調用 newObj.f(...arguments),則 this.name = 'lily'等同於 newObj.name = 'lily',mockNew 返回 newObj 時,p 就等於 newObj,所以 p 可以訪問到 person 的 name 屬性。
function person() {
this.name = 'lily';
}
//這裏,能夠認爲mockNew(person)等同於new person()
var p = mockNew(person);
p.name // lily
複製代碼
來看另外一個例子:
var human = {
name: 'lucy',
}
//返回了一個對象,則new操做符最終返回這個對象
function person() {
this.name = 'lily';
return human;
}
var p = mockNew(person);
p.name // lucy
複製代碼
由此,經過 new 操做符調用構造函數時,this 的最終指向爲 new 返回的對象,即新建立的對象 newObj 或構造函數中返回的對象 returnObj(上例中的 human)。
func.call 和 func.apply
func.call 和 func.apply 的做用同樣都是改變執行上下文,只是接收參數的形式不一樣。 func.apply 方法傳入兩個參數,第一個參數是想要指定的上下文,爲空則指向全局對象,第二個參數是函數參數組成的數組。 func.call 方法傳入兩個參數,第一個參數是想要指定的上下文,第二個參數是傳入的是一個參數列表,而不是單個數組。
/*
thisArg: 想要指定的環境
argsArray: 參數數組
*/
func.apply(thisArg, argsArray)
/*
thisArg: 想要指定的環境
arg一、arg2...: 參數列表
*/
func.call(thisArg, arg1, arg2, ...)
複製代碼
以下 boy 並無 getName 方法,可是經過 apply/call 改變 this 的指向達到了在 boy 中調用 girl 的 getName 方法。
function getName(firstName, lastName) {
console.log(`${firstName}.${this.name}.${lastName}`)
};
const girl = {
name: 'lucy',
getName,
};
const boy = {
name: 'Jeffrey'
};
//至關於boy.getName(['Michael', 'Jordan'])
girl.getName.apply(boy, ['Michael', 'Jordan']);
girl.getName.call(boy, 'Michael', 'Jordan');
=> Michael.Jeffrey.Jordan
複製代碼
bind 函數
bind 方法不會當即執行,而是返回一個改變了上下文 this 後的函數。
const newGetName = girl.getName.bind(boy);
newGetName('Michael', 'Jordan')
=> Michael.Jeffrey.Jordan
複製代碼
綜上,this 的指向由其具體的執行環境決定,同時也能夠經過函數的原型方法 apply、call 以及 bind 來顯式地改變 this 的指向。不過,箭頭函數的this,老是指向定義時所在的對象,而不是運行時所在的對象,apply、call也沒法更改。