1. 神秘的this
大多數時間this關鍵字都充滿了神秘的色彩對我和許多JavaScript開發人員。這是一個強大的功能特性,但是它需要努力才能被理解。
從Java、PHP還有其他標準語言的背景,在類方法裡,this是被當做當前對象的一個執行個體,不能多也不能少。大多數情況下,他不能在方法之外使用,這種簡單的策略不會對this的理解造成混亂。
在JavaScript中情況有所不同:this是當前函數的執行環境(execution context of a funciton).JavaScript有4種函數調用方式: 函數調用: alert(‘Hello World’) 方法調用: console.log(‘Hello World’) 構造器調用: new RegExp(‘\d’) 間接調用: alert.call(undefined, ‘Hello World!’)
每種調用方式定義了它們自己的執行環境,所以this與開發人員的期望有略微不同。
此外,strict模式也會影響執行內容。
理解this關鍵字的關鍵是函數調用有一個清晰的看法以及它如何影響上下文。
文本的重點是調用的解釋,函數調用如何影響this,並示範了識別內容相關的常見陷阱。
開始之前,讓我們先來熟悉一些術語: 一個函數的調用是正在執行構成函數體的代碼, 或者簡單的調用。例如parseInt函數調用是parseInt(‘15’) 調用的上下文是函數體內this的值。例如map.set(‘key’, ‘value’)的調用有調用上下文map 一個函數的範圍是函數體內可訪問的變數、對象、函數的集合。 2. 函數調用
函數調用在對一個函數對象求值的運算式後跟一個左括弧(,逗號分隔的參數運算式列表和右括弧)時執行。例如 parseInt(‘18’)
函數調用運算式不能是屬性訪問器(例如console.log(‘hi’)),它是方法調用。再例如[1,5].join(’,’)不是一個函數調用,而是一個方法調用。這種區別是很重要的。
函數調用的一個簡單的例子:
function hello(name) { return 'Hello ' + name + '!';}// 函數調用var message = hello('World');console.log(message);// => 'Hello World!'
上述hello(‘World’)是一個函數調用: hello運算式求值到一個函數對象,緊跟著一對括弧中帶著參數‘World’
更進階的例子是IIFE(immediately-invoked function expression):
var message = (function(name) { return 'Hello ' + name + '!';})('World');console.log(message) // => 'Hello World!'
IIFE也是一個函數調用: 第一對括弧(function(name) {…})是一個被定義為函數的運算式,緊跟著一對括弧中帶著參數‘World’ 2.1. 函數調用中的this
在一次函數調用中, this是一個全域對象
全域對象是在執行環境中被確定的。在瀏覽器中,他是window對象
window;function myFunc() { console.log(window == this);// true;}
在一次函數調用,執行內容就是全域對象。
讓我們檢查下面函數的上下文:
function sum(a, b) { console.log(this == window);// true this.myNumber = 20; // 添加一個叫'myNumber'的屬性到全域對象中 return a + b;}// sum() 作為函數被調用// this 在sum() 中是一個全域對象(window)sum(15, 16); // => 31window.myNumber; // => 20
當sum(15, 16)被調用時, JavaScript自動化佈建this為全域對象,在瀏覽器中它是window
當this在函數之外被使用時,this也為全域對象:
console.log(this === window); // => true this.myString = 'Hello World!'; console.log(window.myString); // => 'Hello World!'
<!-- In an html file --> <script type="text/javascript"> console.log(this === window); // => true</script>
2.2. strict 模式(strict mode)下,函數調用中的this
在strict 模式(strict mode)下,一次函數調用中, this是一個undefined
在ECMAScript 5.1中引入了strict 模式(strict mode),它是JavaScript的一個有限變體。它提供更好的安全性和更強的錯誤檢查。
要啟用strict 模式(strict mode),請將指令“use strict”放在函數體的頂部
一旦啟用,strict 模式(strict mode)影響執行內容,使其在常規函數調用中未定義。執行內容不再是全域對象,與上面的情況2.1相反。
function myFunc() { 'use strict'; console.log(this == undefined);// =>true}myFunc();
函數strict 模式下被執行的例子:
function multiply(a, b) { 'use strict'; // 啟用strict 模式 console.log(this === undefined); // => true return a * b;}// this的值是undefinedmultiply(2, 5); // => 10
strict 模式開啟後不僅僅作用在當前執行環境,也作用在內建函式執行環境。也就是說內建函式也在strict 模式下被執行。
function execute() { 'use strict'; // activate the strict mode function concat(str1, str2) { // the strict mode is enabled too console.log(this === undefined); // => true return str1 + str2; } // concat() is invoked as a function in strict mode // this in concat() is undefined concat('Hello', ' World!'); // => "Hello World!"}execute();
單個JavaScript檔案可能包含非strict 模式與strict 模式並存。因此在同一個調用類型的單個指令碼中可能有不同的上下文行為:
function nonStrictSum(a, b) { // non-strict mode console.log(this === window); // => true return a + b;}function strictSum(a, b) { 'use strict'; // strict 模式啟動 console.log(this === undefined); // => true return a + b;}// nonStrictSum函數內this是windownonStrictSum(5, 6); // => 11 // strictSum函數內this是undefinedstrictSum(8, 12); // => 20
2.3. 陷阱:內建函式中的this 錯誤的認知: 函數調用的一個常見的陷阱是認為
this在內建函式與外部函數相同 正確的認知: 正確地,內建函式的上下文僅依賴於調用,而不依賴於外部函數的上下文。
var numbers = { numberA: 5, numberB: 10, sum: function() { console.log(this === numbers); // => true function calculate() { // this是window或者是undefined,如果是在strict 模式下的話。 console.log(this === numbers); // => false return this.numberA + this.numberB; } return calculate(); }};numbers.sum(); // => 普通模式下NaN,在strict 模式下會報錯
為瞭解決這個問題, calculate這個函數應該被sum方法相同的執行內容執行,可以用下面這種方法來指定執行內容:
var numbers = { numberA: 5, numberB: 10, sum: function() { console.log(this === numbers); // => true function calculate() { console.log(this === numbers); // => true return this.numberA + this.numberB; } // 使用.call() 方法來修改執行內容 return calculate.call(this); }};numbers.sum(); // => 15
3. 方法調用
方法是儲存在對象的屬性中的函數。例如:
var myObject = { // helloFunction 它就是一個方法 helloFunction: function() { return 'Hello World!'; }}var message = myObject.helloFUnction();
helloFunction就是myObject的一個方法。擷取方法,用屬性訪問器: myObject.helloFunction.
方法調用在屬性訪問器的形式中的運算式執行,該運算式計算為函數對象後面跟著一個開啟的括弧(,逗號分隔的參數運算式列表和右括弧)。
回憶前面的例子,myObject.helloFunction()是對象myObject上的helloFunction的方法調用。方法調用也是:[1,2] .join(’,’)或/\s/.test(‘beautiful world’)。
將函數調用跟方法調用區分開很重要,因為它們是不同的類型。主要的區別是方法調用需要一個屬性訪問器來調用函數(obj.myFunc()),而函數調用不需要(myFunc())。
下面的的調用列表展現了怎樣區分它們的不同:
['Hello', 'World'].join(', '); // 方法調用({ ten: function() { return 10; } }).ten(); // 方法調用var obj = {}; obj.myFunction = function() { return new Date().toString();};obj.myFunction(); // 方法調用var otherFunction = obj.myFunction; otherFunction(); // 函數調用parseFloat('16.60'); // 函數調用isNaN(0); // 函數調用
理解函數調用和方法調用的差異能協助我們標識正確的上下文。 3.1. 方法調用中的this
this是在方法調用中擁有方法的對象
當在一個對象中調用一個方法時,this變成了對象它本身。
var myObject = { myMethod: function() { this; }}myObject.myMethod();
讓我們建立一個帶一個增加數位方法的對象:
var calc = { num: 0, increment: function() { console.log(this === calc); // true this.num += 1; return this.num; }};// 方法調用calc.increment(); // => 1calc.increment(); // => 2
調用calc.increment()使increment函數的上下文為calc對象。所以使用this.num來增加number屬性是很好的。
讓我們看看其他情況。一個JavaScript對象從它的原型繼承一個方法。當這個被繼承的方法在這對象中被調用時,調用的上下文仍然是它自己本身。
var myDog = Object.create({ sayName: function() { console.log(this === myDog); // => true return this.name; }});myDog.name = 'Milo'; // method invocation. this is myDogmyDog.sayName(); // => 'Milo'
3.2. 陷阱:分離方法與其對象 錯誤的認知: 從一個對象的方法可以提取到一個單獨的變數var alone = myObj.myMethod。當單獨調用該方法時,從原始對象alone()分離,您可能認為這是定義方法的對象。 正確的認知: 如果沒有對象調用該方法,則會發生函數調用:這是全域對象視窗或在strict 模式下未定義(參見2.1和2.2)
以下樣本建立Animal建構函式並建立它的執行個體 - myCat。然後setTimout()1秒後記錄myCat對象資訊:
function Animal(type, legs) { this.type = type; this.legs = legs; this.logInfo = function() { console.log(this === myCat); // => false console.log('The ' + this.type + ' has ' + this.legs + ' legs'); }}var myCat = new Animal('Cat', 4); // 顯示 "The undefined has undefined legs" 或者拋出異常(在strict 模式下)setTimeout(myCat.logInfo, 1000); // 顯示 "The Cat has 4 legs"setTimeout(myCat.logInfo.bind(myCat), 1000);
4. 構造器調用
當new關鍵字後面是一個運算式,其值為一個函數對象,一個開啟的括弧(,逗號分隔的參數運算式列表和一個右括弧)時,將執行構造方法調用。例如:new RegExp(’\ d’)。
function Country(name, traveled) { this.name = name ? name : 'United Kingdom'; this.traveled = Boolean(traveled);}Country.prototype.travel = function() { this.traveled = true;};// 構造器調用var france = new Country('France', false); // 構造器調用var unitedKingdom = new Country;france.travel(); // 法國旅行
new Country(‘France’, false)是一個Country函數的構造器調用。這個執行結果是一個新的對象,它的name屬性是‘France’
如果構造器沒有參數,括弧可以被省略: new Country 4.1. 構造器調用中的this
this是在構造器調用中新建立的對象
構造器調用的上下文是剛建立了的對象。它用來初始化對象的資料, 從構造器函數參數, 設定初始的屬性值還有一些處理方法等。
functioin Constructor() { this;// 自身對象}var object = new Constructor();// object跟上面的this是同一個對象。
讓我們來看看下面的例子的上下文:
function Foo() { console.log(this instanceof Foo); // => true this.property = 'Default Value';}// 構造器調用var fooInstance = new Foo();fooInstance.property; // => 'Default Value'
new Foo()被當做一個構造器調用, 它的上下文是fooInstance。在Foo內部,對象被初始化:this.property被賦予一個預設值。
當new Foo()被執行的時候, JavaScript建立一個Null 物件並且讓它成為構造方法的上下文。之後你可以使用this關鍵字添加屬性到對象: this.property = ‘Default Value’ 4.2. 陷阱:忘記了new
一些JavaScript函數建立執行個體不僅用構造器,而且用函數調用。例如RegExp:
var reg1 = new RegExp('\\w+');var reg2 = RegExp('\\w+');reg1 instanceof RegExp; // => truereg2 instanceof RegExp; // => truereg1.source === reg2.source; // => true
function Vehicle(type, wheelsCount) { if(!(this instanceof Vehicle) { throw Error('Error: 錯誤調用'); } this.type = type; this.wheelsCount = wheelsCount; return this;}var car = new Vehicle('Car', 4);car.type;car.wheelsCount;car instanceof Vehicle;// 函數調用。會產生一個錯誤var brokenCar = Vehicle('Broken Car', 3);
new Vehicle(‘Car’, 4)運行正常: 一個新的對象被建立並且被初始化, 因為new關鍵字存在於構造器調用中 5. 間接調用
間接調用: 當一個函數使用myFun.call()或者myFun.apply被調用時,將執行間接調用。
JavaScript中的函數是第一類對象,這意味著一個函數是一個對象。此對象的類型是Function。
Function對象的兩個方法:
call(thisArg[, arg1[, arg2[, …]]])
apply(thisArg, [arg1, arg2, …])
例如:
myFun.call(thisValue, 'val1', 'val2');myFunc.apply(thisValue, ['val1', 'val2']);
5.1. 間接調用中的this
間接調用this是.call()或者.apply()的第一個參數
下面的例子顯示了間接調用的上下文:
var rabbit = { name: '小白兔' }; function concatName(string) { console.log(this === rabbit); // => true return string + this.name;}// 間接調用concatName.call(rabbit, '你好 '); // => '你好 小白兔' concatName.apply(rabbit, ['再見 ']); // => '再見 小白兔'
間接調用在某些時候非常有用,比如在strict 模式下,我們的上下文是window或者undefined的時候,使用間接調用,函數體內的this就能夠是我們想要的對象。 6. 綁定函數
綁定函數是一個攜帶著對象的函數。通常,它都是通過原始函數使用.bind()函數來建立的。通過此方法來改變函數的執行環境
function multiply(number) { 'use strict'; return this * number;}// create a bound function with contextvar double = multiply.bind(2); // invoke the bound functiondouble(3); // => 6 double(10); // => 20
6.1. 綁定函數中的this
綁定單數中的this是.bind()函數的第一個參數。
var numbers = { array: [3, 5, 10], getNumbers: function() { return this.array; }};// 建立一個綁定函數var boundGetNumbers = numbers.getNumbers.bind(numbers); boundGetNumbers(); // => [3, 5, 10] // 提取方法var simpleGetNumbers = numbers.getNumbers; simpleGetNumbers(); // => undefined 或者 拋出異常在strict 模式下
6.2. 緊密上下文綁定
.bind()建立一個永久上下文連結並始終保持它。
當使用.call()或.apply()與不同的上下文時,綁定的函數不能更改其連結的上下文,或者甚至反彈沒有任何效果。只有綁定函數的建構函式調用可以改變,但是這不是推薦的方法(對於建構函式調用,使用正常,不綁定的函數)。
以下樣本建立一個bound函數,然後嘗試更改其已預定義的上下文:
function getThis() { 'use strict'; return this;}var one = getThis.bind(1); // 綁定函數調用one(); // => 1 // 用 .apply() .call() 調用綁定函數one.call(2); // => 1 one.apply(2); // => 1 // 再次綁定one.bind(2)(); // => 1 // 將綁定函數通過構造器來調用new one(); // => Object
只有new()改變綁定的函數的上下文,其他類型的調用不會改變函數的執行內容。 7. 箭頭函數
箭頭函數旨在以較短的形式聲明函數,並以詞法綁定上下文。
它可以使用以下方式:
var hello = (name) => { return 'Hello ' + name;};hello('World'); // => 'Hello World' // 只保留偶數[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]
箭頭函數帶來一個更輕的文法,可以不包括詳細關鍵字function。當函數只有1個語句,你甚至可以省略返回。
一個箭頭函數是匿名的,這意味著name屬性是一個Null 字元串”。這樣,它沒有詞法函數名(這對於遞迴,分離事件處理常式很有用)。
它也不支援arguments對象,跟普通函數不一樣。然後他可以支援剩餘參數在ES2015之後:
var sumArguments = (...args) => { console.log(typeof arguments); // => 'undefined' return args.reduce((result, item) => result + item);};sumArgume