譯者按: 總結了大量JavaScript基本知識點,很有用!
為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用於學習。
根據StackOverflow調查, 自2014年一來,JavaScript是最流行的程式語言。當然,這也在情理之中,畢竟1/3的開發工作都需要一些JavaScript知識。因此,如果你希望在成為一個開發者,你應該學會這門語言。
這篇部落格的主要目的是將所有面試中常見的概念總結,方便你快速去了解。(鑑於本文內容過長,方便閱讀,將分為三篇部落格來翻譯, 此為第二部分。第一部分請點選快速掌握JavaScript面試基礎知識(一))
閉包
閉包由一個函式以及該函式定義是所在的環境組成。我們通過例子來形象解釋它。
function sayHi(name){
var message = `Hi ${name}!`;
function greeting() {
console.log(message)
}
return greeting
}
var sayHiToJon = sayHi('Jon');
console.log(sayHiToJon) // ƒ() { console.log(message) }
console.log(sayHiToJon()) // 'Hi Jon!'
複製程式碼
請理解var sayHiToJon = sayHi('Jon');
這行程式碼的執行過程,sayHi
函式執行,首先將message
的值計算出來;然後定義了greeting
函式,函式中引用了message
變數;最後,返回greeting
函式。
如果按照C/Java語言的思路,sayHiToJon
就等價於greeting
函式,那麼會報錯:message未定義。但是在JavaScript中不一樣,這裡的sayHiToJon
函式等於greeting
函式以及一個環境,該環境中包含了message
。因此,當我們呼叫sayHiToJon
函式,可以成功地將message
列印出來。因此,這裡的閉包就是greeting
函式和一個包含message
變數的環境。(備註: 為了便於理解,此段落未按照原文翻譯。)
閉包的一個優勢在於資料隔離。我們同樣用一個例子來說明:
function SpringfieldSchool() {
let staff = ['Seymour Skinner', 'Edna Krabappel'];
return {
getStaff: function() { console.log(staff) },
addStaff: function(name) { staff.push(name) }
}
}
let elementary = SpringfieldSchool()
console.log(elementary) // { getStaff: ƒ, addStaff: ƒ }
console.log(staff) // ReferenceError: staff is not defined
/* Closure allows access to the staff variable */
elementary.getStaff() // ["Seymour Skinner", "Edna Krabappel"]
elementary.addStaff('Otto Mann')
elementary.getStaff() // ["Seymour Skinner", "Edna Krabappel", "Otto Mann"]
複製程式碼
在elementary
被建立的時候,SpringfieldSchool
已經返回。也就是說staff
無法被外部訪問。唯一可以訪問的方式就是裡面的閉包函式getStaff
和addStaff
。
我們來看一個面試題:下面的程式碼有什麼問題,如何修復?
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log(`The value ${arr[i]} is at index: ${i}`);
}, (i+1) * 1000);
}
複製程式碼
上面的程式碼輸出的結果全部都一樣:"The value undefined is at index: 4"。因為所有在setTimeout
中定義的匿名函式都引用了同一個外部變數i
。當匿名函式執行的時候,i
的值為4。
這個問題可以改用IIFE
(後面會介紹)方法來解決,通過對每一個匿名函式構建獨立的外部作用域來實現。
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
(function(j) {
setTimeout(function() {
console.log(`The value ${arr[j]} is at index: ${j}`);
}, j * 1000);
})(i)
}
複製程式碼
當然,還有一個方法,使用let
來宣告i
。
const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log(`The value ${arr[i]} is at index: ${i}`);
}, (i) * 1000);
}
複製程式碼
立即呼叫的函式表示式(Immediate Invoked Function Expression)(IIFE)
一個IIFE是一個函式表示式在定義之後立即被呼叫。常用在你想對一個新宣告的變數建立一個隔離的作用域。
它的格式為: (function(){....})()
。前面的大括號用於告訴編譯器這裡不僅僅是函式定義,後面的大括號用於執行該函式。
var result = [];
for (var i=0; i < 5; i++) {
result.push( function() { return i } );
}
console.log( result[1]() ); // 5
console.log( result[3]() ); // 5
result = [];
for (var i=0; i < 5; i++) {
(function () {
var j = i; // copy current value of i
result.push( function() { return j } );
})();
}
console.log( result[1]() ); // 1
console.log( result[3]() ); // 3
複製程式碼
使用IIFE可以:
- 為函式繫結私有資料
- 建立一個新的環境
- 避免汙染全域性名稱空間
環境(Context)
我們往往容易將環境(Context)和作用域(Scope)搞混,我來簡單解釋一下:
- 環境(Context): 由函式如何被呼叫而決定,往往指
this
。 - 作用域(Scope): 可訪問的變數。
函式呼叫:call, apply, bind
這三個方法都是為了將this繫結到函式,區別在於呼叫的方式。
.call()
會立即執行函式,你需要把引數按順序傳入;.apply()
會立即執行函式,你需要把所有的引數組合為一個陣列傳入;
.call()
和.apply()
幾乎相同。哪個傳入引數方便,你就選擇哪個。
const Snow = {surename: 'Snow'}
const char = {
surename: 'Stark',
knows: function(arg, name) {
console.log(`You know ${arg}, ${name} ${this.surename}`);
}
}
char.knows('something', 'Bran'); // You know something, Bran Stark
char.knows.call(Snow, 'nothing', 'Jon'); // You know nothing, Jon Snow
char.knows.apply(Snow, ['nothing', 'Jon']); // You know nothing, Jon Snow
複製程式碼
注意:如果你將陣列傳入call
函式,它會認為只有一個引數。
ES6允許使用新的操作符將陣列變換為一個序列。
char.knows.call(Snow, ...["nothing", "Jon"]); // You know nothing, Jon Snow
複製程式碼
.bind()
返回一個新的函式,以及相應的環境和引數。如果你想該函式稍後呼叫,那麼推薦使用bind
。
.bind()
函式的優點在於它可以記錄一個執行環境,對於非同步呼叫和事件驅動的程式設計很有用。
.bind()
傳引數的方式和call
相同。
const Snow = {surename: 'Snow'}
const char = {
surename: 'Stark',
knows: function(arg, name) {
console.log(`You know ${arg}, ${name} ${this.surename}`);}
}
const whoKnowsNothing = char.knows.bind(Snow, 'nothing');
whoKnowsNothing('Jon'); // You know nothing, Jon Snow
複製程式碼
this關鍵字
要理解JavaScript中this
關鍵字,特別是它指向誰,有時候相當地複雜。this
的值通常由函式的執行環境決定。簡單的說,執行環境指函式如何被呼叫的。this
像是一個佔位符(placeholder),它指向當方法被呼叫時,呼叫對應的方法的物件。
下面有序地列出了判斷this
指向的規則。如果第一條匹配,那麼就不用去檢查第二條了。
-
new
繫結 - 當使用new
關鍵字呼叫函式的時候,this
指向新構建的物件。function Person(name, age) { this.name = name; this.age =age; console.log(this); } const Rachel = new Person('Rachel', 30); // { age: 30, name: 'Rachel' } 複製程式碼
-
顯示繫結(Explicit binding) - 當使用
call
或則apply
的時候,我們顯示的傳入一個物件引數,該引數會繫結到this
。 注意:.bind()
函式不一樣。用bind
定義一個新的函式,但是依然繫結到原來的物件。function fn() { console.log(this); } var agent = {id: '007'}; fn.call(agent); // { id: '007' } fn.apply(agent); // { id: '007' } var boundFn = fn.bind(agent); boundFn(); // { id: '007' } 複製程式碼
-
隱式繫結 - 當一個函式在某個環境下呼叫(在某個物件裡),
this
指向該物件。也就是說該函式是物件的一個方法。var building = { floors: 5, printThis: function() { console.log(this); } } building.printThis(); // { floors: 5, printThis: function() {…} } 複製程式碼
-
預設繫結 - 如果上面所有的規則都不滿足,那麼
this
指向全域性物件(在瀏覽器中,就是window物件)。當函式沒有繫結到某個物件,而單獨定義的時候,該函式預設繫結到全域性物件。function printWindow() { console.log(this) } printWindow(); // window object 複製程式碼
注意:下面的情況中,inner
函式中的this
指向全域性。
function Dinosaur(name) {
this.name = name;
var self = this;
inner();
function inner() {
alert(this); // window object — the function has overwritten the 'this' context
console.log(self); // {name: 'Dino'} — referencing the stored value from the outer context
}
}
var myDinosaur = new Dinosaur('Dino');
複製程式碼
- 詞法(Lexical) this - 當是使用
=>
來定義函式時,this
指向定義該函式時候外層的this
。 備註:大概是和定義的詞法(=>
)有關,把它稱作Lexical this
。
function Cat(name) {
this.name = name;
console.log(this); // { name: 'Garfield' }
( () => console.log(this) )(); // { name: 'Garfield' }
}
var myCat = new Cat('Garfield');
複製程式碼
嚴格(Strict)模式
如果你使用了"use strict"
指令,那麼JavaScript程式碼會在嚴格模式下執行。在嚴格模式下,對於詞法分析和錯誤處理都有特定的規則。在這裡我列出它的一些優點:
- 使得Debug更容易:以前會被忽略的錯誤現在會顯示報錯,比如賦值給一個不可寫的全域性變數或則屬性;
- 避免不小心宣告瞭全域性變數:賦值給一個未定義的變數會報錯;
- 避免無效使用delete:嘗試去刪除變數、函式或則不可刪除的屬性會丟擲錯誤;
- 避免重複的屬性名和引數值:物件上重複的屬性和函式引數會丟擲錯誤(在ES6中不再是這樣);
- 使得
eval()
更加安全:在eval()
中定義的變數和函式在外部作用域不可見; - “安全”的消除JavaScript中this的轉換:如果
this
是null或則undefined不在轉換到全域性物件。也就是說在瀏覽器中使用this去指向全域性物件不再可行。
對於在嚴格(strict)模式和測試階段都沒有發現的bug,不妨接入線上實時監控外掛Fundebug。
版權宣告:
轉載時請註明作者Fundebug以及本文地址:
https://blog.fundebug.com/2018/01/22/the-definitive-javascript-handbook-for-a-developer-interview-2/