👇 內容速覽 👇
- let和const
- Set和Map
- Generator和yield
- Promise、async/await介紹
- Proxy代理器
- ...
let和const
ES6新增了let
和const
,它們聲明的變量,都處於“塊級作用域”。並且不存在“變量提升”,不允許重複聲明。
同時,const
聲明的變量所指向的內存地址保存的數據不得改變:
- 對於簡單類型的數據(數值、字符串、布爾值),值就保存在變量指向的那個內存地址,因此等同於常量。
- 對於複合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指向實際數據的指針,
const
只能保證這個指針是固定的(即總是指向另一個固定的地址),不能保證指向的數據結構不可變。
如果要保證指向的數據結構也不可變,需要自行封裝:
/**
* 凍結對象
* @param {Object} obj
* @return {Object}
*/
function constantize(obj) {
if(Object.isFrozen(obj)) {
return obj
}
Reflect.ownKeys(obj).forEach(key => {
// 如果屬性是對象,遞歸凍結
typeof obj[key] === 'object' && (obj[key] = constantize(obj[key]))
});
return Object.freeze(obj)
}
/********測試代碼 **********/
const obj = {
a: 1,
b: {
c: 2,
d: {
a: 1
}
},
d: [
1,
2
]
}
const fronzenObj = constantize(obj)
try {
fronzenObj.d = []
fronzenObj.b.c = 3
} catch(error) {
console.log(error.message)
}
Set和Map
題目:解釋下Set
和Map
。
- Set元素不允許重複
- Map類似對象,但是它的鍵(key)可以是任意數據類型
①Set常用方法
// 實例化一個set
const set = new Set([1, 2, 3, 4]);
// 遍歷set
for (let item of set) {
console.log(item);
}
// 添加元素,返回Set本身
set.add(5).add(6);
// Set大小
console.log(set.size);
// 檢查元素存在
console.log(set.has(0));
// 刪除指定元素,返回bool
let success = set.delete(1);
console.log(success);
set.clear();
其他遍歷方法:由於沒有鍵名,values()
和keys()
返回同樣結果。
for (let item of set.keys()) {
console.log(item);
}
for (let item of set.values()) {
console.log(item);
}
for (let item of set.entries()) {
console.log(item);
}
②Map常用方法
Map接口基本和Set一致。不同的是增加新元素的API是:set(key, value)
const map = new Map();
// 以任意對象爲 Key 值
// 這裏以 Date 對象爲例
let key = new Date();
map.set(key, "today");
console.log(map.get(key));
Generator與yield
generator
函數是es6提供的新特性,它的最大特點是:控制函數的執行。讓我們從網上最火的一個例子來看:
function* foo(x) {
var y = 2 * (yield x + 1);
var z = yield y / 3;
return x + y + z;
}
var b = foo(5);
b.next(); // { value:6, done:false }
b.next(12); // { value:8, done:false }
b.next(13); // { value:42, done:true }
通俗的解釋下爲什麼會有這種輸出:
- 給函數foo傳入參數5,但由於它是generator,所以執行到第一個yield前就停止了。
- 第一次調用next(),這次傳入的參數會被忽略暫停**。
- 第二次調用next(12),傳入的參數會被當作上一個yield表達式的返回值。因此,y = 2 * 12 = 24。執行到第二個yield,返回其後的表達式的值 24 / 3 = 8。然後函數在此處暫停。
- 第三次調用next(13),沒有yield,只剩return了,按照正常函數那樣返回return的表達式的值,並且
done
爲true
。
難點:在於爲什麼最後的value
是42呢?
首先,x
的值是剛開始調用foo函數傳入的5。而最後傳入的13被當作第二個yield的返回值,所以z
的值是13。對於y
的值,我們在前面第三步中已經計算出來了,就是24。
所以,x + y + z = 5 + 24 + 13 = 42
看懂了上面的分析,再看下面這段代碼就很好理解了:
function* foo(x) {
var y = 2 * (yield x + 1);
var z = yield y / 3;
return x + y + z;
}
var a = foo(5);
a.next(); // Object{value:6, done:false}
a.next(); // Object{value:NaN, done:false}
a.next(); // Object{value:NaN, done:true}
只有第一次調用next函數的時候,輸出的value是6。其他時候由於沒有給next傳入參數,因此yield的返回值都是undefined
,進行運算後自然是NaN
。
Promise介紹
簡單歸納下 Promise:三個狀態、兩個過程、一個方法
- 三個狀態:
pending
、fulfilled
、rejected
-
兩個過程(單向不可逆):
-
pending
->fulfilled
-
pending
->rejected
-
- 一個方法
then
:Promise
本質上只有一個方法,catch
和all
方法都是基於then
方法實現的。
請看下面這段代碼:
// 構造 Promise 時候, 內部函數立即執行
new Promise((resolve, reject) => {
console.log("new Promise");
resolve("success");
});
console.log("finifsh");
// then 中 使用了 return,那麼 return 的值會被 Promise.resolve() 包裝
Promise.resolve(1)
.then(res => {
console.log(res); // => 1
return 2; // 包裝成 Promise.resolve(2)
})
.then(res => {
console.log(res); // => 2
});
async/await介紹
async
函數返回一個Promise
對象,可以使用then
方法添加回調函數。
當函數執行的時候,一旦遇到await
就會先返回,等到異步操作完成,再接着執行函數體內後面的語句。
這也是它最受歡迎的地方:能讓異步代碼寫起來像同步代碼,並且方便控制順序。
可以利用它實現一個sleep
函數阻塞進程:
function sleep(millisecond) {
return new Promise(resolve => {
setTimeout(() => resolve, millisecond)
})
}
/**
* 以下是測試代碼
*/
async function test() {
console.log('start')
await sleep(1000) // 睡眠1秒
console.log('end')
}
test() // 執行測試函數
雖然方便,但是它也不能取代Promise
,尤其是我們可以很方便地用Promise.all()
來實現併發,而async/await
只能實現串行。
function sleep(second) {
return new Promise(resolve => {
setTimeout(() => {
console.log(Math.random());
resolve();
}, second);
});
}
async function chuanXingDemo() {
await sleep(1000);
await sleep(1000);
await sleep(1000);
}
async function bingXingDemo() {
var tasks = [];
for (let i = 0; i < 3; ++i) {
tasks.push(sleep(1000));
}
await Promise.all(tasks);
}
運行bingXingDemo()
,幾乎同時輸出,它是併發執行;運行chuanXingDemo()
,每個輸出間隔1s,它是串行執行。
ES6對象和ES5對象
題目:es6 class 的new實例和es5的new實例有什麼區別?
在ES6
中(和ES5
相比),class
的new
實例有以下特點:
-
class
的構造參數必須是new
來調用,不可以將其作爲普通函數執行 -
es6
的class
不存在變量提升 -
最重要的是:es6內部方法不可以枚舉。es5的
prototype
上的方法可以枚舉。
爲此我做了以下測試代碼進行驗證:
console.log(ES5Class()) // es5:可以直接作爲函數運行
// console.log(new ES6Class()) // 會報錯:不存在變量提升
function ES5Class(){
console.log("hello")
}
ES5Class.prototype.func = function(){ console.log("Hello world") }
class ES6Class{
constructor(){}
func(){
console.log("Hello world")
}
}
let es5 = new ES5Class()
let es6 = new ES6Class()
// 推薦在循環對象屬性的時候,使用for...in
// 在遍歷數組的時候的時候,使用for...of
console.log("ES5 :")
for(let _ in es5){
console.log(_)
}
// es6:不可枚舉
console.log("ES6 :")
for(let _ in es6){
console.log(_)
}
參考/推薦:《JavaScript創建對象—從es5到es6》
Proxy代理器
他可以實現js中的“元編程”:在目標對象之前架設攔截,可以過濾和修改外部的訪問。
它支持多達13種攔截操作,例如下面代碼展示的set
和get
方法,分別可以在設置對象屬性和訪問對象屬性時候進行攔截。
const handler = {
// receiver 指向 proxy 實例
get(target, property, receiver) {
console.log(`GET: target is ${target}, property is ${property}`)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
console.log(`SET: target is ${target}, property is ${property}`)
return Reflect.set(target, property, value)
}
}
const obj = { a: 1 , b: {c: 0, d: {e: -1}}}
const newObj = new Proxy(obj, handler)
/**
* 以下是測試代碼
*/
newObj.a // output: GET...
newObj.b.c // output: GET...
newObj.a = 123 // output: SET...
newObj.b.c = -1 // output: GET...
運行這段代碼,會發現最後一行的輸出是 GET ...
。也就是說它觸發的是get
攔截器,而不是期望的set
攔截器。這是因爲對於對象的深層屬性,需要專門對其設置Proxy。
更多請見:《阮一峯ES6入門:Proxy》
EsModule和CommonJS的比較
目前js社區有4種模塊管理規範:AMD、CMD、CommonJS和EsModule。 ES Module 是原生實現的模塊化方案,與 CommonJS 有以下幾個區別:
- CommonJS 支持動態導入,也就是
require(${path}/xx.js)
,後者目前不支持,但是已有提案:import(xxx)
- CommonJS 是同步導入,因爲用於服務端,文件都在本地,同步導入即使卡住主線程影響也不大。而後者是異步導入,因爲用於瀏覽器,需要下載文件,如果也採用同步導入會對渲染有很大影響
- commonJs輸出的是值的淺拷貝,esModule輸出值的引用
- ES Module 會編譯成
require/exports
來執行的
更多系列文章
《前端知識體系》
- JavaScript基礎知識梳理(上)
- JavaScript基礎知識梳理(下)
- ES6重難點整理
- 談談promise/async/await的執行順序與V8引擎的BUG
- 前端面試中常考的源碼實現
- Flex上手與實戰
- ......
《設計模式手冊》
《Webpack4漸進式教程》