第一章 塊級作用域綁定
let
和 const
都是不存在提升,聲明的都是塊級標識符
都禁止重聲明
var a = 30;
var message = 2;
// 這兩條都會拋出語法錯誤
let a = 40;
const message = 1;
每個const
聲明的常量必須進行初始化
const
定義的常量不能修改,但是用const聲明的對象可以修改值
const name; // 語法錯誤:常量未初始化
const person = {
name: 'a'
};
person.name = 'b'; // 可以修改
// SyntaxError: "person" is read-only
person = {
name: 'Jone'
}
臨時死區(Temporal Dead Zone)
let
和const
聲明不會像var
一樣提升到作用域頂部,如果在聲明之前訪問這些變量,即使是相對安全的typeof
操作符也會觸發引用錯誤。用let
來舉例(const也一樣)
if(1){ // 加不加if(1)結果一樣
// Uncaught ReferenceError: value is not defined
console.log(typeof value);
let value = 1;
}
console.log(typeof value2); // "undefined"
if(1){
let value2 = 1;
}
由于console.log(typeof value)
語句會拋出錯誤,因此用let定義并初始化變量value的語句不會執行。此時的value還位于JavaScript社區所謂的臨時死區TDZ中。
JavaScript引擎在掃描代碼發現變量聲明時,要么將它們提升至作用域頂部(var),要么把聲明放到TDZ中(let、const)。訪問TDZ中的變量會觸發運行時錯誤。只有執行過變量聲明語句后,變量才會從TDZ中移除,方可正常訪問。
上述的第二個例子 typeof 是在聲明變量value2的代碼塊外執行的,此時value2并不在TDZ中,這也就意味著不存在value2這個綁定,typeof返回undefined
循環中的let
沒什么可說的,不用再使用IIFE了
循環中的const
for(const i=0; i<10; i++){ // Uncaught TypeError: Assignment to constant variable.
console.log(i)
}
var object = {a:1,b:1,c:1}; // 不會報錯
for(const key in object){
console.log(key)
}
第一個 for循環必然會報錯
第二個 不會報錯的原因是for-in和for-of循環中,每次迭代不會像for循環一樣修改已有的綁定,而是會創建一個新綁定
全局塊作用域綁定
在全局作用域中,var 聲明的變量會成為全局對象(瀏覽器環境中的window)的屬性。這意味著var很可能會無意中覆蓋一個已經存在的全局變量。
var Test=1;
window.Test === Test; // true
如果在全局作用域中使用let或const,會在全局作用域下創建一個新的綁定,但該綁定不會添加為全局對象的屬性。換句話說,用let
或const
不能覆蓋全局變量,而只能遮蔽它。
const foo = 1;
window.foo = 2;
console.log(foo); // 1
console.log(window.foo); // 2
塊級綁定最佳實踐的進化
ECMAScript6標準尚在開發中時,人們普遍認為應該默認使用let
而不是var
。對很多JavaScript開發者而言,let
實際上與他們想要的var
一樣,直接替換符合邏輯。這種情況下,對于需要些保護的變量則要使用const。
然而,當更多開發者遷移到ECMAScript6后,另一種做法日益普及:默認使用const,只有確實需要改變變量的值時使用let
。因為大部分變量的值在初始化后不應再改變,而預料外的變量值的改變是很多bug的源頭。
第二章 字符串和正則表達式
暫略
第三章 函數
1. 函數形參的默認值
JavaScript函數有一個特別的地方,無論在函數定義中聲明了多少形參,都可以傳入任意數量的參數,也可以在定義函數時添加針對參數數量的處理邏輯,當已定義的形參無對應的傳入參數時為期指定一個默認值。
1.1 ECMAScript5中模擬默認參數
function makeRequest(url, timeout, callback){
timeout = timeout || 2000;
callback = callback || function(){}
}
這樣做的一個缺陷是 如果想指定給timeout的值就是0,也會被賦值2000。所以更穩妥的辦法是使用typeof
檢查參數類型
function makeRequest(url, timeout, callback){
timeout = (typeof timeout !== 'undefined')? timeout : 2000;
callback = (typeof callback !== 'undefined')? callback : function(){}
}
在流行的JavaScript庫中均使用類似的模式進行補全。
1.2 ECMAScript6中的默認參數值
function makeRequest(url, timeout=2000, callback=function(){}){
}
聲明函數時,可以為任意參數指定默認值,在已指定默認值的參數后可以繼續聲明無默認值參數。如上例,callback可以無默認值。
1.3 默認參數值對arguments對象的影響
ES5非嚴格模式下,函數命名參數的變化會體現在arguments對象中
function mixArgs(first, second){
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
first = 'c';
second = 'd';
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
}
嚴格模式下,first和second的值不會導致arguments改變。
function mixArgs(first, second){
"use strict";
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
first = 'c';
second = 'd';
console.log(first === arguments[0]); //false
console.log(second === arguments[1]); //false
}
在ES6函數使用默認參數值時,無論是否顯示定義了嚴格模式,arguments對象的行為都將與ES5嚴格模式下保持一致。默認參數值的存在使得arguments對象保持與命名參數分離。
function mixArgs(first, second = 'b'){
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = 'c';
second = 'd';
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs('a');
// 1 true false false false false
1.4 默認參數表達式
關于默認參數值,最有趣的挺特性可能是非原始值傳參。
function getValue(){
return 5;
}
function add(first, second = getValue()){
return first + second;
}
console.log(add(1,1)); // 2
console.log(add(1)); // 6
這段代碼中,如果不傳入最后一個參數,就會調用getValue()函數來得到正確的默認值。切記初次解析函數生命時,不會調用getValue()方法,只有當調用add()函數且不傳入第二個參數時才會調用
let value = 5;
function getValue(){
return value++;
}
function add(first, second = getValue()){
return first + second;
}
add(1,1); // 2
add(1); // 6
add(1); // 7
注意,當使用函數調用結果作為默認參數值時,如果忘記寫小括號,例如,second=getValue, 則最終傳入的是對函數的引用,而不是函數調用的結果。
正因為默認參數實在函數調用時求值,所以可以使用先定義的參數作為后定義參數的默認值(反過來不可以)
function add(first, second = first){
return first + second;
}
add(1, 1); // 2
add(1); // 2
1.5 默認參數的臨時死區
第一章介紹let
和const
時我們介紹了臨時死區TDZ,其實默認參數也有同樣的臨時死區,在這里的參數不可訪問。與let
聲明類似,定義參數時會為每個參數創建一個新的標識符綁定,該綁定在初始化之前不可被引用,如果試圖訪問會導致程序拋出錯誤。當調用函數時,會通過傳入的值或參數的默認值初始化該參數。
function getValue(value){
return value + 5;
}
function add(first, second = getValue(first)){
return first + second;
}
add(1,1); // 2
add(1); // 7
調用add(1, 1)和add(1)時實際相當于執行以下代碼來創建fist和second參數值:
// add(1,1)時執行的JavaScript代碼
let first = 1;
let second = 1;
// add(1)時的JavaScript代碼
let first = 1;
let second = getValue(first);
1.4節中提到過不可以 后定義的參數作為先定義參數的默認值。
function add(first=senond, second){
return first + second;
}
add(1,1) //2
// 執行的函數
// let first = 1;
// let second = 1;
add(undefined, 1) //拋出錯誤
// 執行的函數
// let first = second;
// let second = 1;
可見,調用add(undefined, 1)函數時,因為當first初始化時second尚未初始化,所以會導致程序拋出錯誤,此時second尚處于臨時死區中。
函數參數有自己的作用域和臨時死區,與其函數體的作用域是各自獨立的,也就是說參數的默認值不可訪問函數體內聲明的變量。
2. 處理無命名參數
到目前為止,本章中的示例使用到的參數都是命名參數。然而JavaScript的函數語法規定,無論函數已定義的命名參數有多少毛豆不限制調用時傳入的實際參數數量,調用時總是可以傳入任意數量的參數。
2.1 ES5中的無命名參數
下面pick函數模仿了Underscore.js庫中的pick()方法,返回一個給定對象的副本,包含原始對象屬性的特定子集。
function pick(object) {
let result = Object.create(null);
for (let i = 1, len = arguments.length; i < len; i++) {
result[arguments[i]] = object[arguments[i]];
}
return result;
}
let book = {
title: "Understanding ECMAScrpt6",
author: "NowhereToRun",
year: "2017"
};
let bookData = pick(book, "author", "year");
console.log(bookData);
關于pick函數應該注意這樣幾件事情:
首先,并不容易發現這個函數可以接受任意數量的參數。
其次,因為第一參數為命名參數并且已被使用,當你要查找需要拷貝的屬性名稱時,不得不從索引1而不是索引0開始遍歷arguments對象。
而在ES6中,通過不定參數(rest parameters)的特性可以解決這個問題。
2.2 不定參數
function pick(object, ...keys) {
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
不定參數keys包含的是object知乎傳入的所有參數(而arguments對象包含的則是所有傳入的參數,包括object)這樣一來你就可以放心地遍歷keys對象了。這種方法還有一個好處,秩序看一眼函數就可以知曉該函數可以處理的參數數量。
函數的length屬性統計的是函數命名參數的數量,不定參數的加入不會影響length屬性的值。在本例中,pick函數的length值為1,因為只會計算object。(即與改寫成為使用不定參數的函數之前一樣)
2.3 不定參數的使用限制
- 每個函數最多只能聲明一個不定參數,而且一定要放在所有參數的末尾
- 不定參數不能用于對象字面量setter之中
function pick(object, ...keys , last) {
// SyntaxError: Rest parameter must be last formal parameter
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
let object = {
// SyntaxError: Setter function argument must not be a rest parameter
set name(...name){
// 執行一些邏輯
}
}
2.4 不定參數對arguments對象的影響
無論是否使用不定參數,arguments對象總是包含所有傳入函數的參數。
3. 增強的Function構造函數
Function構造函數是JavaScript語法中很少被使用到的一部分,通常我們用它來動態創建新的函數。這種構造函數接受字符串形式的參數,分別為函數的參數及函數體。
var add = new Function("first", "second", "return first + second");
console.log(add(1, 1)); // 2
ES6中增強了Function構造函數的功能,支持在創建函數時定義默認參數和不定參數。唯一需要做的是在參數名后面添加一個等號及一個默認值。
var add = new Function("first", "second = first", "return first + second");
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
定義不定參數,只需在最后一個參數前添加...
var pickFirst = new Function("...args","return args[0]");
console.log(pickFirst(1,2,3,4)); // 1
4. 展開運算符
簡單粗暴,看個例子,不解釋太多
let values = [25,50,75,100];
console.log(Math.max.apply(Math, values));
console.log(Math.max(...values));
可以將展開運算符與其他正常傳入的參數混合使用。例如像限定返回的最小值為0.
let values = [-25, -50, -75, -100];
console.log(Math.max(...values, 0));
5. name屬性
由于在JavaScript中有多種定義函數的方式,因而辨別函數就是一項具有挑戰性的任務,而且匿名函數表達式的廣泛使用更是加大了調試難度。于是ES6中為所有的函數新增了name屬性。
function doSomething() { }
var doAnotherSomething = function () { };
console.log(doSomething.name); // doSomething
console.log(doAnotherSomething.name); // doAnotherSomething
var doSth = function doSthElse() { };
var person = {
get firstName() {
return "HeiHeiHei"
},
sayName: function () {
console.log(this.name);
}
}
console.log(doSth.name); // doSthElse
console.log(person.sayName.name); // sayName
console.log(person.firstName.name); // get firstName (undefined)
var bindName = function(){};
console.log(bindName.bind().name); // bound bindName
console.log((new Function).name); // anonymous
前兩個沒什么說的。
第三個 doSth,可見權重。(書上原話:函數表達式有一個名字,這個名字比函數本身被賦值的變量的權重高,感覺好像不是一個意思)
第五個firstName,目前Chrome(59.0.3)測試是undefined
第六個,綁定函數的name屬性總是由被綁定函數的name屬性及字符串前綴bound
組成
第七個,通過Function構造函數創建的函數,其名稱將帶有前綴anonymous
切記,函數name屬性的值不一定引用同名變量,他只是協助調試用的額外信息,所以不能使用name屬性的值來獲取對于函數的引用
6. 明確函數的多重用途
ES5及早期版本中的函數具有多重功能,可以結合new使用,函數內的this值將指向一個新對象,函數最終會返回這個新對象。
function Person(name){
this.name = name;
}
var person = new Person("HaHa");
var noAPerson = Person("HaHa");
console.log(person); // Person {name: "HaHa"}
console.log(noAPerson); // undefined
console.log(name); // HaHa
給noAPerson賦值時,沒有通過new關鍵字來調用Person(),最終返回undefined(因為是非嚴格模式,全局下會設置name屬性,嚴格模式下會直接報錯)。通過new關鍵字調用Person()時才體現其能力。
在ES6中,函數混亂的雙重身份終于將有一些改變。
JavaScript函數有兩個不同的內部方法[[Call]]和[[Construct]]。當通過new關鍵字調用時執行的是[[Construct]]函數,它負責創建一個通常稱為治理的新對象,然后在執行函數體,將this綁定到示例上;如果不通過new來調用函數,則執行[[Call]]函數,從而直接執行代碼中的函數體。具有[[Construct]]方法的函數被統稱為構造函數。
切記,不是所有函數都有[[Construct]]方法,因此不是所有函數都可以用new來調用,比如箭頭函數。
ES5中判斷函數被調用的方法
為了確定函數是被new調用,通常使用instancsof,但是也不完全可靠。
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
return new Person(name);
// 或者直接拋出錯誤
// throw new Error("必須通過new關鍵字來調用")
}
}
var person = new Person("HaHa");
var person2 = Person("HaHa");
console.log(person); // Person {name: "HaHa"}
console.log(person2); // Person {name: "HaHa"}
// 下面這種寫法會錯誤的執行,而且會修改person的值
var noAPerson = Person.call(person, "HeiHei");
console.log(noAPerson); // undefined
console.log(person); // Person {name: "HaHa"}
ES6 元屬性(Metaproperty)new.target
為了解決判斷函數是否通過new關鍵字調用的問題,ES6引入了new.target這個元屬性。元屬性是指非對象的屬性,其可以提供非對象目標的補充信息(例如new)。當調用函數的[[Construct]]方法時,new.target被賦值為new操作符的目標,通常是新創建對象實例,也就是函數體內this的構造函數;如果調用[[Call]]方法,則new.target的值為undefined。
function Person(name){
if(typeof new.target === Person){
this.name = name;
} else{
throw new Error("必須通過new關鍵字來調用");
}
}
var person = new Person("haha");
console.log(person); // Person {name: "haha"}
var notAPerson = Person.call(person, "HeiHei"); // Error: 必須通過new關鍵字來調用
在函數外使用new.target是一個語法錯誤
7. 塊級函數
在ES3和早起版本中,在代碼塊中聲明一個塊級函數嚴格來說是一個語法錯誤,但是所有的瀏覽器仍然支持這個特性。但是很不幸,每個瀏覽器對這個特性的支持都稍有不同,所以最好不要使用這個特性(最好的選擇是使用函數表達式)。
為了遏制這種互相不兼容的行為,ES5嚴格模式中引入了一個錯誤提示,當在代碼塊內部聲明函數時程序會拋出錯誤:
"use strict";
if(1){
// 在ES5中拋出語法錯誤,在ES6中不報錯
function doSomething(){}
}
在ES6中,會把doSomething視作一個塊級聲明,從而可以在定義該函數的代碼塊內訪問和調用它。
在定義函數的代碼塊內,塊級函數會提升至頂部
"use strict";
if (1) {
console.log(typeof doSomething); // function
function doSomething() { }
doSomething();
}
console.log(typeof doSomething); // undefined
但是let定義的函數表達式不會提升至頂部
"use strict";
if (1) {
console.log(typeof doSomething); // ReferenceError: doSomething is not defined
let doSomething = function () { }
doSomething();
}
console.log(typeof doSomething);
非嚴格模式下的塊級函數
與嚴格模式下稍有不同,這些函數不在提升至代碼塊的頂部,而是提升至外圍函數或全局作用域的頂部
if (1) {
console.log(typeof doSomething); // function
function doSomething() { }
doSomething();
}
console.log(typeof doSomething); // function
8. 箭頭函數
箭頭函數與傳統的JavaScript函數有些許不同,主要集中在以下方面:
- 沒有this、super、arguments和new.target綁定 箭頭函數中的這些值由外圍最近一層非建投函數決定。
- 不能通過new調用 箭頭函數沒有[[Construct]]方法,所以不能被用作構造函數,如果通過new關鍵字調用建投函數,程序會報錯。
- 沒有原型 由于不可以通過new方法調用,因而沒有構建原型的需求,所以箭頭函數不存在prototype這個屬性。
- 不可以改變this的綁定 函數內部的this值不可以被改變,在函數的生命周期內始終保持一致。
- 不支持arguments對象
- 不支持重復的命名參數 無論在嚴格還是費嚴格模式下,建投函數都不支持重復的命名參數;而在傳統函數的規定中,只有在嚴格模式下才會不能有重復的命名參數。
箭頭函數同樣也有一個name屬性,這與其他函數的規則相同。
箭頭函數語法
當箭頭函數只有一個參數時,可以直接寫參數名,箭頭緊隨其后,箭頭右側的表達式被求值后便立即返回,即使沒有顯示的返回語句,這個箭頭函數也可以返回傳入的第一個參數,不需要更多的語法鋪墊。
let reflect = value => value;
// 相當于(babel轉換后的代碼)
var reflect = function reflect(value) {
return value;
};
如果傳入兩個或兩個以上的參數,要在參數的兩側添加一對小括號:
let sum = (num1, num2) => num1 + num2;
// 相當于
var sum = function sum(num1, num2) {
return num1 + num2;
};
如果沒有參數也要在聲明的時候寫一個小括號
let getNmae = () => "NowhereToRun";
// 相當于
var getNmae = function getNmae() {
return "NowhereToRun";
};
如果函數有多個語句,可以像傳統的函數體一樣使用花括號包裹起來
let sum2 = (num1, num2) => {
let temp = num1 + num2;
return temp * num1;
}
// 相當于
var sum2 = function sum2(num1, num2) {
var temp = num1 + num2;
return temp * num1;
};
空函數
let doNothing = () => { };
// 相當于
var doNothing = function doNothing() {};
想在箭頭函數外返回一個對象字面量,則需要將該字面量包裹在小括號里(為了將其與函數體區分開來)
let getTempItem = id => ({ id: id, name: "Temp" });
// 相當于
var getTempItem = function getTempItem(id) {
return { id: id, name: "Temp" };
};
箭頭函數沒有this綁定
下面這段代碼PageHandler設計初衷是用來處理頁面上的交互,init初始化。
let PageHandler = {
id : "123456",
init: function(){
document.addEventListener('click', function(event){
this.doSomething(event.type); // Uncaught TypeError: this.doSomething is not a function
}, false);
},
doSomething: function(type){
console.log("Handling " + type + " for " + this.id);
}
};
但是this.doSomething(event.type);
中的this綁定的是document
(因為是document負責了調用)。可以使用bind強行綁定
let PageHandler = {
id : "123456",
init: function(){
document.addEventListener('click', (function(event){
this.doSomething(event.type);
}).bind(this), false);
},
doSomething: function(type){
console.log("Handling " + type + " for " + this.id);
}
};
使用bind總覺的有些奇怪,因為他實際上創建了另一個函數。可以使用箭頭函數來修正。
箭頭函數沒有this綁定,必須通過查找作用域鏈來決定其值。如果箭頭函數被非箭頭函數包含,this綁定的是最近一層費箭頭函數的this。否則this的值會被設置為undefined。
let PageHandler = {
id: "123456",
init: function () {
document.addEventListener('click', event =>
this.doSomething(event.type)
, false);
},
doSomething: function (type) {
console.log("Handling " + type + " for " + this.id);
}
};
此處的this和init函數里的this一致。
箭頭函數缺少正常函數所擁有的prototype屬性,它的設計初衷是“即用即棄”,所以不能用他來定義新的類型。
var MyType = () => {};
new MyType(); // MyType is not a constructor
同樣,箭頭函數中的this值取決于該函數外部非箭頭函數的this值,且不能通過call、apply、或bind方法來改變this值。(使用不會報錯,但是無效)
箭頭函數沒有arguments綁定
箭頭函數沒有自己的arguments對象,且未來無論在哪個上下文中執行,箭頭函數始終可以訪問外圍函數的arguments對象。
function createArrowFunctionReturningFirstArg(){
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5
8. 尾調用優化
ES6關于函數最有趣的變化可能是尾調用系統的引擎優化。
function doSomething (){
return doSomethingElse(); // 尾調用
}
在ES5中,尾調用的實現與其他函數小勇的實現類似:創建一個新的棧幀(stack frame),將其推入調用棧來表示函數調用。也就是說,在循環調用中,每一個未用完的幀都會被保存在內存中,當調用站變得過大時會造成程序問題。
ES6中的尾調用優化
ES6中縮減了嚴格模式下尾調用棧的大小(非嚴格模式下不受影響),如果滿足一下條件,尾調用不再創建新的棧幀,而是清除并重用當前棧幀:
- 尾調用不訪問當前棧幀的變量(也就是說函數不是一個閉包)。
- 在函數內部,尾調用是最后一條語句。
- 尾調用的結果作為函數值返回。
以下代碼滿足以上三個條件,可以被JavaScript引擎自動優化:
"use strict";
function doSomething (){
return doSomethingElse();
}
如果做一個小的改動,不返回最終結果,那么引擎就無法優化當前函數:
"use strict";
function doSomething (){
doSomethingElse();
}
同樣地,如果你定義了一個函數,在尾調用返回后執行其他操作,則函數也無法得到優化:
"use strict";
function doSomething (){
return 1 + doSomethingElse();
}
在上面這個示例中,在返回doSomethingElse()的結果前將其加1,折足以去優化空間。
還有另外一種意外情況,如果把函數調用的結果存儲在一個變量里,最后再返回這個變量,則可能導致引擎無法優化:
function doSomething (){
var result = doSomethingElse();
return result;
}
可能最難避免的情況是閉包的使用,它可以訪問作用域中的所有變量,因而導致尾調用優化失效:
"use strict";
function doSomething() {
var num = 1,
func = () => num;
// 無法優化,這是一個閉包
return func();
}
如何利用尾調用優化
實際上,尾調用的優化發生在引擎背后,除非你嘗試優化一個函數,否則無需思考此類問題。遞歸函數是其最主要的應用場景,此時尾調用優化的效果最顯著。
"use strict";
function factorial(n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
由于在遞歸時執行了乘法操作,因而當前版本的階乘函數無法被引擎優化。如果n是一個非常大的數,則調用棧的尺寸就會不斷增長并存在最終導致棧溢出的潛在風險。
優化這個函數,首先要確保乘法不會在函數調用后執行,你可以通過默認參數來將乘法操作移除return語句,結果函數可以攜帶著臨時結果進入到下一個迭代中。下面這段代碼可以被ES6引擎優化:
"use strict";
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
return factorial(n - 1, result);
}
}