ECMAScript概述
JavaScript = ECMAScript + 運行環境的API
其中,運行環境包括瀏覽器和Node環境
ES2015(ES6來泛指ES2015之後的新標準)新特性
解決原有用語法上的不足
let、const與塊級作用域
- let、const聲明的變量只在{}包裹的塊級作用域內有效
// 將會打印出三次foo;
for(let i = 0; i < 3; i ++) {
let i = 'foo';
console.log(i);
}
// 這段代碼相當於如下
// 第一步,聲明循環變量
let i = 0;
// 第二步,判斷循環條件,執行循環體,由於循環體是一個塊級作用域,因此與循環變量儘管同名,但在循環體內只有一個i變量;
if(i < 3) {
let i = 'foo';
console.log(i);
}
// 第三步,循環變量的值增加
i ++;
- 不會有作用域提升效果,也就是隻有執行到聲明語句時,纔有這個變量;
- const用來聲明常量值,意味着該變量只讀;對於引用類型而言,該常量值爲引用地址,只要不更改引用地址就行;
在程序中,不用var,主要用const,再配合let
for of 遍歷
作爲遍歷所有數據結構的統一方式,在Iterator對象調用next方法後返回的結果對象中如果done屬性爲false,則執行循環體,否則退出循環。
- 可以在遍歷過程中使用break關鍵字終止循環。
- 可以遍歷數組、類數組集合、Set、Map等等,但不能遍歷對象,原因在於對象沒有實現默認的Iterator接口。
- 對Map數據結構的遍歷時,得到的元素是鍵值對構成的二元數組,因此可以使用數組解構賦值的方式來使用鍵值對。
對原有語法的增強
解構賦值
更加簡潔地從一個複雜的數據結構中提取值,包括數組、對象這些數據結構。
數組解構
將一個已知數組中的值賦值給變量時,爲了提升效率,不再利用索引一個一個賦值,而是一次性賦值。
解構時要根據位置(即數字索引)來賦值。
- 支持使用…擴展符解構
- 支持在解構時,爲變量提供默認值
const arr = [1, 2, 3];
// 原始做法
const foo = arr[0];
const bar = arr[1];
const baz = arr[2];
// 解構做法
const [foo, bar, baz] = arr; // foo = 1, bar = 2, baz = 3;
// 或者只賦值某一部分
const [, , baz] = arr; // baz = 3;
// 使用...擴展符解構,...rest這種方式只能用於數組末尾
const [foo, ...rest] = arr; // foo = 1, rest = [2, 3];
// 解構時使用默認值,如果解構賦值時沒有得到值,則使用默認值
const [foo, bar, baz, more = 'default'] = arr; // more = 'default';
對象解構
使用屬性名(索引)來解構賦值。
const obj = { name: 'mm', age: 18 };
const { name: foo = 'ff', age: bar } = obj; // foo = 'mm', bar = 18, 其中foo = 'ff'爲默認賦值
模板字符串
使用``來標識字符串
- 支持在字符串中加入換行符(而不是用\n)
- 支持插值表達式,不需要再用醜陋的拼接方式
- 支持標籤函數,標籤函數用於對模板字符串進行提取,並進行額外加工;
const str = console.log`hello world`; // [ 'hello world' ];
const name = 'tom';
const gender = true;
// 自定義標籤函數
// 標籤函數接收的第一個參數爲,模板字符串被插值表達式分隔而成的字符串數組
// 標籤函數接收的其他參數爲,模板字符串中插值表達式的值
function myTag(strings, name, gender) {
console.log(strings, name, gender); // strings = ['hello, ', ' is a ', '.'], name = 'tom', gender = true;
const sex = gender ? 'man': 'woman';
return strings[0] + name + strings[1] + sex + strings[2]; // 'hello, tom is a men.';
}
const result = myTag`hello, ${name} is a ${gender}.`;
字符串方法擴展
- includes(str):判斷字符串是否包含str
- startsWith(str):判斷字符串是否以str開頭
- endsWith(str):判斷字符串是否以str結尾
函數擴展
參數默認值
- 帶默認值的參數要放在函數形參的末尾
剩餘參數
使用擴展符…rest作爲形參來接收函數的實參,其中rest爲數組,剩餘參數…rest要放在形參的末尾
使用擴展符來傳遞實參
const arr = ['foo', 'baz', 'bar'];
// 通過...arr傳遞實參
console.log(...arr);
箭頭函數
即lambda表達式形式
- 簡化了回調函數的編寫
- 突出了函數的本質(類型到類型的映射關係,適用於函數式編程範式)
- 不會改變this指向,箭頭函數內部沒有this機制,因此箭頭函數體的this綁定的是函數上下文中的this,即箭頭函數定義的詞法作用域中的this;在使用保存this指向的場景下,都可以使用箭頭函數來簡化
const person1 = {
name: 'tom',
sayHi: () => {
setTimeout(function() {
console.log(this.name);
}, 1000)
}
}
person1.sayHi(); // undefined;
const person2 = {
name: 'tom',
// 注意在方法的定義上,不要使用箭頭函數,否則內部的this找不到
sayHi: function() {
// 使用_this來保存this指向,通過閉包機制訪問到this.name;
let _this = this;
setTimeout(function() {
console.log(_this.name);
}, 1000)
}
}
person2.sayHi(); // tom;
const person3 = {
name: 'tom',
sayHi: function() {
setTimeout(() => {
console.log(this.name);
}, 1000)
}
}
person3.sayHi(); // tom;
對象增強
動態屬性名即計算屬性名
在對象用字面量方式定義時,可以利用[表達式]的方式去定義動態屬性名,稱之爲計算屬性名
對象擴展方法
- Object.assign(target, source1, source2, …rest):將source1、source2等對象的屬性複製到target對象,並返回target對象。同名屬性將會被覆蓋,覆蓋以從右向左的順序。注意:淺複製。
- 一般常用於更新對象,但不修改源對象的非侵入式修改,適合函數式編程範式。
- 這也是JavaScript擴展對象的一種方式 Mixin 。
- assign方法無法複製源對象的訪問器屬性,該方法會將訪問器屬性執行之後,將執行結果作爲數據屬性複製到目標對象上。
- Object.is(value1, value2): 比較兩個值是否全等。主要是爲了補充+0與-0、NaN與NaN的比較。因爲在符號
===
的情況下,與Object.is方法剛好相反的。Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
- Object.keys(obj):返回目標對象obj所有可枚舉的屬性名組成數組
- Object.getOwnPropertyNames(obj):返回目標對象obj不考慮可枚舉性的屬性名組成的數組
- Object.getOwnPropertySymbols(obj):返回目標對象obj所有Symbol類型的屬性名組成的數組
全新的對象及其API
Proxy代理對象
用處
用於攔截程序中對對象的訪問、設置等各種行爲,使用代理對象對這些行爲進行響應。每一種特定的行爲都有相應的陷阱來攔截該行爲,並指定對應的處理程序來響應該行爲。響應程序可以自定義,也可以使用Reflect API來使用Proxy默認的響應行爲。
通過Proxy代理之後,對該對象的操作轉而需要通過Proxy來進行。
構造
const person = {
name: 'tom',
age: 18
}
// new Proxy(target, options)傳入代理的目標對象和代理配置對象,配置對象內部定義了各種攔截器
const personProxy = new Proxy(person, {
// 攔截訪問對象屬性值的行爲,target爲要訪問的目標對象,property爲要訪問的屬性名
get (target, property) {
// 不管訪問目標對象的什麼屬性,一律返回100; 這裏就是自定義的響應;
return 100;
}
// 攔截設置對象屬性值的行爲,增加校驗行爲
set (target, property, value) {
// 可以針對age屬性做一下校驗
if(property === 'age') {
if(!Number.isInteger(value)) {
throw new TypeError(`${value} is not an Integer!`);
}
target[property] = value;
}
target[property] = value;
}
})
// 訪問person.name的行爲轉而變爲訪問personProxy.name的行爲;
const result = peronProxy.name; // 100;
與Object.defineProperty的對比
- Object.defineProperty只能攔截對屬性的讀寫操作,Proxy可以攔截更多的操作,這取決於Proxy內部的陷阱函數
- 監視數組的操作
- Object.defineProperty,如Vue中是通過重寫數組的操作方法來攔截
- Proxy 監視數組的操作更加靈活
- 非侵入式攔截對象操作
const list = [];
const listProxy = new Proxy(list, {
set(target, property, value) {
console.log('set', property, value);
target[property] = value;
return true; // 表示寫入成功
}
})
// push操作可以被Proxy的set陷阱攔截,並且property自動更新;但如果是利用Object.defineProperty時,就需要重寫數組的push方法了,原來的push方法無法被攔截到;
list.push(66); // set, 0, 66;
list.push(88); // set, 1, 88;
Reflect
靜態類,封裝一系列的對對象的底層操作API,是Proxy對象攔截行爲的默認響應,每一個API都與Proxy中的陷阱函數相對應。我們在定義Proxy中的陷阱函數時,可以先自定義好需要的邏輯,然後使用Reflect的API將默認響應返回。
Reflect的意義在於,統一了針對對象的各種操作,不管是利用Object的靜態方法還是實例方法,還是一些操作符如in, has等,都可以通過Reflect API找到對應的方法;Reflect API可以不和Proxy一起使用,也可以單獨使用,目的是取代之前定義的對象操作行爲,統一經過Reflect API來操作。
class類
靜態成員
使用static關鍵字聲明靜態成員,內部的this指向類本身(構造函數)
繼承
使用extends關鍵字實現類型繼承
- 在子類構造函數中需要先調用super()函數初始化this值,才能在之後的代碼中使用this。super引用父類構造函數,是構造函數借用。
Iterator對象
用於提供統一的迭代數據結構的接口,是for of 實現遍歷的前提。也就是說,只有數據結構實現了Iterator接口,才能夠被for of方式來遍歷。
數據結構如果實現了[Symbol.iterator]屬性(屬性值爲函數),該函數返回一個Iterator對象。Iterator對象的next()方法用來獲取數據結構中的下一個元素。這也是for of 方式實現遍歷的原理。
爲對象實現Iterator接口
只要實現了Iterator接口,就可以使用for of 遍歷;
const obj {
store: ['foo', 'bar', 'baz'],
// 實現iterable接口,用於返回Iterator對象
[Symbol.iterator]: function() {
const self = this; // 保存對象的引用;
let index = 0;
// 實現Iterator對象,用於迭代對象內部的值
return {
next: function() {
const result = {
value: self.store[index],
done: index >= self.store.length
}
index ++;
return result;
}
}
}
}
迭代器模式管理數據結構的遍歷
Iterator對象是爲了實現迭代器模式而創建。迭代器模式是爲了將遍歷代碼與數據結構解耦,統一使用迭代器來進行遍歷。這樣如果數據結構頻繁變化時,可以只修改迭代器即可,遍歷代碼改動很小。也就是說,迭代器模式將數據結構與遍歷代碼搭建了一個統一的橋樑。
Generator生成器
意義
提供更好的異步編程寫法
yield關鍵字
使用generator實現Iterator
const obj {
store: ['foo', 'bar', 'baz'],
// 實現iterable接口,用於返回Iterator對象,這裏使用了生成器函數
[Symbol.iterator]: function* () {
for(const item of this.store) {
yield item;
}
}
}
redux-saga使用generator寫法
全新的數據類型
Set數據結構
數據集合,其中的數據不重複。
構造
const set = new Set();
操作方法(均爲實例方法)
- add(item):增加一個元素item到Set
- forEach((item, index) => {}):遍歷Set中的元素
- size屬性:返回Set中元素的數量
- has(item):判斷Set中item是否存在
- delete(item):刪除Set中item元素
- clear():清理Set所有元素
WeakSet
Map數據結構(對象的增強或補充)
key-value數據集合,Map與對象的區別是,key可以是任意數據類型,而對象的key只能是字符串(其他數據類型會被隱式轉換爲字符串類型);
構造
const map = new Map();
操作方法(均爲實例方法)
- get(key)
- set(key, value)
- forEach((value, key) => {})
- 其他類似於對象的方法,如keys()、values()、entries()等
WeakMap
弱引用集合。
- WeakMap的鍵必須是一個對象,使用非對象的鍵會報錯
- 集合中保存的是這些對象的弱引用,如果在弱引用之外的不存在其他的強引用,則垃圾回收機制會將弱引用對象收回,同時從WeakMap中刪除對應的鍵值對。
- 最大的用於是保存DOM元素。當DOM元素從頁面移除時(外部強引用不存在了),WeakMap內部對應的DOM元素也會被垃圾回收機制自動清除。
Symbol類型
意義
- 創建獨一無二的值
const s = Symbol(); // 不能使用new創建,而是直接用Symbol()
,每次使用Symbol()創建出來的值都不相同。- 創建私有屬性
- 避免第三方庫的命名衝突
創建
- 給
Symbol(str)
傳入可選的str字符串,用於描述這個Symbol類型的值。描述文本有利於開發調試,讓開發者知道該值的相關信息。 - Symbol的描述文本存儲在內部的[[ Descriptor ]]屬性,只有調用Symbol的toString()方法纔可以讀取到這個內部屬性。使用console.log()時隱式調用了toString()方法。
const s1 = Symbol('name');
console.log(s1); // "Symbol('name')"
類型判斷
可以使用typeof操作符來判斷一個值是否是Symbol類型
const s1 = Symbol('symbol test');
console.log(typeof s1); // 'symbol'
對象私有屬性的創建
- 對象的屬性名現在可以接受string類型和Symbol類型了
- 通過Symbol類型可以創建對象的私有屬性了,因爲外部無法找到相同的Symbol類型值。
const name = Symbol(); // name變量中保存了新創建的Symbol類型值,但name變量不暴露給外部即可實現私有屬性 const obj = { [name]: 'foo', say() { console.log(this.[name]); } }
Symbol共享體系
在代碼中查找所創建的Symbol值。全局Symbol註冊表是一個類似全局作用域的共享體系,同樣需要注意避免衝突。
Symbol.for(str):
- 用於查找描述爲str的Symbol值並返回該Symbol類型的值。如果沒找到,則使用str參數創建一個Symbol值並返回。
- 要求傳入str是字符串類型,如果不是則隱式轉換爲字符串類型。
- 爲了查找Symbol值,其內部維護了一個描述文本與值的全局Symbol註冊表,我們可以通過描述文本找到對應的Symbol值。
Symbol.keyFor(symbol):
- 用於查找symbol對應的描述文本。與Symbol.for()方法相反。
- symbol參數要求是一個Symbol值。
- 如果查找不到,則返回undefined。
Symbol與類型轉換
- Symbol類型無法轉換爲字符串類型與數字類型,調用toString()只是返回該值的字符串描述而已。如果Symbol類型與字符串作拼接或參與數學運算都會報錯。
- Symbol可以參與邏輯運算,因爲Symbol等價布爾值爲true
Symbol公共屬性來暴露一些JavaScript內部邏輯
Symbol內置了一些常量屬性來暴露JavaScript的內部邏輯,同時也指定了這些操作的接口。不同的數據類型對這些方法實現情況不同,有的時候需要自定義。如對象類型並沒有[Symbol.iterator]接口。開發者可以根據情況對這些接口進行改寫,這一方式也被稱爲元編程。
Symbol.iterator // 返回迭代器接口的方法
Symbol.hasInstance // 使用instanceof操作符時調用的方法
Symbol.match // 在調用String.prototype.match()方法時調用的方法,用於比較字符串是否匹配子串
Symbol.replace // 在調用String.prototype.replace()方法時調用的方法,用於替換字符串的子串
Symbol.search // 在調用String.prototype.search()方法時調用的方法,用於查詢字符串的子串
Symbol.split // 在調用String.prototype.split()方法時調用的方法,用於分隔字符串
Symbol.isConcatSpreadable // 布爾值,用於表示當傳遞一個集體到Array.prototype.concat()方法的參數時,是否應該將集合內的元素規整到同一層級
Symbol.toPrimitive // 一個返回對象對應的原始類型的值的方法,即有時候特定的操作會觸發將對象轉換爲原始類型的值。
Symbol.toStringTag // 一個在調用Object.prototype.toString()方法時使用的字符串,用於創建對象描述,對象描述指的是[object object]、[object array]之類的。如果定義[Symbol.toStringTag]: value, 則返回的對象描述爲[object value]
Symbol.unscopables // 一個定義了一些不可被with語句引用的對象屬性名稱的對象集合
獲取對象內部的Symbol類型屬性名
使用Object.getOwnPropertySymbols(obj); // 傳入目標對象,返回該對象的Symbol類型的屬性名
來獲取目標對象的Symbol類型的屬性名;
使用傳統的for in
循環或者Object.keys(obj)
只能獲取到字符串類型的屬性名;
同時,使用JSON.stringify(obj)
也不會將Symbol類型屬性序列化;
ES Modules
ES2016
Array.prototype.includes
鑑於Array.prototype.indexOf(NaN)無法判斷數組中的NaN元素,因此擴展出了includes方法
指數運算符
原來:Math.pow(2, 10); // 2^10
現在:2 ** 10;
ES2017
Async/Await寫法
Object的擴展方法
Object.values(obj)
返回目標對象obj屬性值組成數組
Object.entries(obj)
返回目標對象obj鍵值對組成的數組,其中鍵值對也是一個二元數組。
Object.getOwnPropertyDescriptors(obj)
返回目標對象obj上所有屬性以及對應的屬性描述符對象,其結構如下:
{
屬性名1: 屬性描述符對象,
屬性名2: 屬性描述符對象,
...
}
字符串填充
爲給定的字符串在開始處(padStart)或結束處(padEnd)填充指定的字符串,直到填充的字符數達到了指定的字符數。
String.prototype.padStart(num, str)
在給定字符串的開始處填充str(可能會反覆填充str)直到填充後的字符串長度達到num,返回填充後的字符串;
String.prototype.padEnd(num, str)
在給定字符串的結尾處填充str(可能會反覆填充str)直到填充後的字符串長度達到num,返回填充後的字符串;
函數最後一個形式參數後添加逗號
function func(arg1, arg2, arg3,) {
}