ECMAScript規範

語言: CN / TW / HK

theme: channing-cyan highlight: night-owl


這是我參與11月更文挑戰的第16天,活動詳情檢視:[2021最後一次更文挑戰](https://juejin.cn/post/7023643374569816095/ "https://juejin.cn/post/7023643374569816095/") > TIP 👉 **著意栽花花不發,等閒插柳柳成陰。元·關漢卿《包待制智斬魯齋郎**

前言

## ECMAScript規範發展簡介 ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3d17cf9285944b5b8d5d6e7f272b4267~tplv-k3u1fbpfcp-watermark.image?) 相信大家都聽說過ES6, ES7, ES2015, ES2016, ES2017...等等這些錯綜複雜的名字. 有的人以版本號描述, 比如ES6, ES7. 有的人以年份描述, 比如ES2015, ES2016. 那麼他們之間到底是什麼關係? 咱們就要從js的發展看起了. ### 歷史 ECMAScript是由網景的布蘭登·艾克開發的一種指令碼語言的標準化規範;最初命名為Mocha,後來改名為LiveScript,最後重新命名為JavaScript[1]。1995年12月,昇陽與網景聯合發表了JavaScript[2]。1996年11月,網景公司將JavaScript提交給歐洲計算機制造商協會進行標準化。ECMA-262的第一個版本於1997年6月被Ecma組織採納。ECMAScript是由ECMA-262標準化的指令碼語言的名稱。 儘管JavaScript和JScript與ECMAScript相容,但包含超出ECMAScript的功能[3]。 --來自wikipedia. 總之: 嚴格來說, ES6是指2015年6月釋出的ES2015標準, 但是很多人在談及ES6的時候, 都會把ES2016 ES2017等標準的內容也帶進去. 所以嚴謹的說, 在談論ECMAScript標準的時候, 用年份更好一些. 但是也無所謂, 糾結這個沒多大意義。 ### ESNext 前面見到了各種ES6 7, ES2015, 怎麼又冒出一個ESNext? ESNext是什麼? 其實ESNext是一個泛指, 它永遠指向下一個版本. 比如當前最新版本是ES2020, 那麼ESNext指的就是2021年6月將要釋出的標準. ## ES6及以後新增的常用API解析 ### let 和 const 先來一道經典面試題 ```js for(var i=0;i<=3;i++){ setTimeout(function() { console.log(i) }, 10); } ``` 分別會輸出什麼? 為什麼? 如何修改可以使其輸出0,1,2,3? ```js for(var i = 0; i <=3; i++) { (function (i) { setTimeout(function () { console.log(i); }, 10); })(i); } for(let i=0;i<=3;i++){ setTimeout(function() { console.log(i) }, 10); } ``` 原因: var定義的變數是全域性的, 所以全域性只有一個變數i. setTimeout是非同步, 在下一輪事件迴圈, 等到執行的時候, 去找i變數的引用。所以函式找到了遍歷完後的i, 此時它已經變成了4。 1. 而let引入了塊級作用域的概念, 建立setTimeout函式時,變數i在作用域內。對於迴圈的每個迭代,引用的i是i的不同例項。 2. 還存在變數提升的問題 ```js console.log(i) var i = 1; console.log(letI) let letI = 2; ``` 3. const就很簡單了, 在let的基礎上, 不可被修改. ### 箭頭函式 1. 最大的區別:箭頭函式裡的this是定義的時候決定的, 普通函式裡的this是使用的時候決定的。 ```js const teacher = { name: 'lubai', getName: function() { return `${this.name}` } } console.log(teacher.getName()); const teacher = { name: 'lubai', getName: () => { return `${this.name}` } } console.log(teacher.getName()); ``` 2. 簡寫箭頭函式 ```js const arrowFn = (value) => Number(value); console.log(arrowFn('aaa')) ``` 3. 注意, 箭頭函式不能被用作建構函式 建構函式會幹嘛? 改變this指向到新例項出來的物件. 箭頭函式會幹嘛?this指向是定義的時候決定的. ### class ```js class Test { _name = ''; constructor() { this.name = 'lubai'; } static getFormatName() { return `${this.name} - xixi`; } get name() { return this._name; } set name(val) { console.log('name setter'); this._name = val; } } console.log(new Test().name) console.log(Test.getFormatName()) ``` ### 模板字串 ```js const b = 'lubai' const a = `${b} - xxxx`; const c = `我是換行 我換行了! 我又換行了! `; ``` 面試題來一道. 編寫render函式, 實現template render功能. ```js const year = '2021'; const month = '10'; const day = '01'; let template = '${year}-${month}-${day}'; let context = { year, month, day }; const str = render(template)({year,month,day}); console.log(str) // 2021-10-01 function render(template) { return function(context) { return template.replace(/\$\{(.*?)\}/g, (match, key) => context[key]); } } ``` ### 解構 1. 陣列的解構 ```js // 基礎型別解構 let [a, b, c] = [1, 2, 3] console.log(a, b, c) // 1, 2, 3 // 物件陣列解構 let [a, b, c] = [{name: '1'}, {name: '2'}, {name: '3'}] console.log(a, b, c) // {name: '1'}, {name: '2'}, {name: '3'} // ...解構 let [head, ...tail] = [1, 2, 3, 4] console.log(head, tail) // 1, [2, 3, 4] // 巢狀解構 let [a, [b], d] = [1, [2, 3], 4] console.log(a, b, d) // 1, 2, 4 // 解構不成功為undefined let [a, b, c] = [1] console.log(a, b, c) // 1, undefined, undefined // 解構預設賦值 let [a = 1, b = 2] = [3] console.log(a, b) // 3, 2 ``` 2. 物件的結構 ```js // 物件屬性解構 let { f1, f2 } = { f1: 'test1', f2: 'test2' } console.log(f1, f2) // test1, test2 // 可以不按照順序,這是陣列解構和物件解構的區別之一 let { f2, f1 } = { f1: 'test1', f2: 'test2' } console.log(f1, f2) // test1, test2 // 解構物件重新命名 let { f1: rename, f2 } = { f1: 'test1', f2: 'test2' } console.log(rename, f2) // test1, test2 // 巢狀解構 let { f1: {f11}} = { f1: { f11: 'test11', f12: 'test12' } } console.log(f11) // test11 // 預設值 let { f1 = 'test1', f2: rename = 'test2' } = { f1: 'current1', f2: 'current2'} console.log(f1, rename) // current1, current2 ``` 3. 解構的原理是什麼? 針對可迭代物件的Iterator介面,通過遍歷器按順序獲取對應的值進行賦值. 3.1 那麼 Iterator 是什麼? Iterator是一種介面,為各種不一樣的資料解構提供統一的訪問機制。任何資料解構只要有Iterator介面,就能通過遍歷操作,依次按順序處理資料結構內所有成員。ES6中的for of的語法相當於遍歷器,會在遍歷資料結構時,自動尋找Iterator介面。 3.2 Iterator有什麼用? * 為各種資料解構提供統一的訪問介面 * 使得資料解構能按次序排列處理 * 可以使用ES6最新命令 for of進行遍歷 ```js function generateIterator(array) { let nextIndex = 0 return { next: () => nextIndex < array.length ? { value: array[nextIndex++], done: false } : { value: undefined, done: true } }; } const iterator = generateIterator([0, 1, 2]) console.log(iterator.next()) console.log(iterator.next()) console.log(iterator.next()) console.log(iterator.next()) ``` 3.3 可迭代物件是什麼? 可迭代物件是Iterator介面的實現。這是ECMAScript 2015的補充,它不是內建或語法,而僅僅是協議。任何遵循該協議點物件都能成為可迭代物件。可迭代物件得有兩個協議:可迭代協議和迭代器協議。 * 可迭代協議:物件必須實現iterator方法。即物件或其原型鏈上必須有一個名叫Symbol.iterator的屬性。該屬性的值為無參函式,函式返回迭代器協議。 * 迭代器協議:定義了標準的方式來產生一個有限或無限序列值。其要求必須實現一個next()方法,該方法返回物件有done(boolean)和value屬性。 3.4 我們自己來實現一個可以for of遍歷的物件? 通過以上可知,自定義資料結構,只要擁有Iterator介面,並將其部署到自己的Symbol.iterator屬性上,就可以成為可迭代物件,能被for of迴圈遍歷。 ```js const obj = { count: 0, [Symbol.iterator]: () => { return { next: () => { obj.count++; if (obj.count <= 10) { return { value: obj.count, done: false } } else { return { value: undefined, done: true } } } } } } for (const item of obj) { console.log(item) } ``` 或者 ```js const iterable = { 0: 'a', 1: 'b', 2: 'c', length: 3, [Symbol.iterator]: Array.prototype[Symbol.iterator], }; for (const item of iterable) { console.log(item); } ``` ### 遍歷 1. for in 遍歷陣列時,key為陣列下標字串;遍歷物件,key為物件欄位名。 ```js let obj = {a: 'test1', b: 'test2'} for (let key in obj) { console.log(key, obj[key]) } ``` 缺點: * for in 不僅會遍歷當前物件,還包括原型鏈上的可列舉屬性 * for in 不適合遍歷陣列,主要應用為物件 2. for of 可迭代物件(包括 Array,Map,Set,String,TypedArray,arguments物件,NodeList物件)上建立一個迭代迴圈,呼叫自定義迭代鉤子,併為每個不同屬性的值執行語句。 ```js let arr = [{age: 1}, {age: 5}, {age: 100}, {age: 34}] for(let {age} of arr) { if (age > 10) { break // for of 允許中斷 } console.log(age) } ``` 優點: * for of 僅遍歷當前物件 ### Object 1. Object.keys 該方法返回一個給定物件的自身可列舉屬性組成的陣列。 ```js const obj = { a: 1, b: 2 }; const keys = Object.keys(obj); // [a, b] ``` 手寫實現一個函式模擬Object.keys? ```js function getObjectKeys(obj) { const result = []; for (const prop in obj) { if (obj.hasOwnProperty(prop)) { result.push(prop); } } return result; } console.log(getObjectKeys({ a: 1, b: 2 })) ``` 2. Object.values 該方法返回一個給定物件自身的所有可列舉屬性值的陣列。 ```js const obj = { a: 1, b: 2 }; const keys = Object.keys(obj); // [1, 2] ``` 手寫實現一個函式模擬Object.values? ```js function getObjectValues(obj) { const result = []; for (const prop in obj) { if (obj.hasOwnProperty(prop)) { result.push(obj[prop]); } } return result; } console.log(getObjectValues({ a: 1, b: 2 })) ``` 3. Object.entries 該方法返回一個給定物件自身可列舉屬性的鍵值對陣列。 ```js const obj = { a: 1, b: 2 }; const keys = Object.entries(obj); // [ [ 'a', 1 ], [ 'b', 2 ] ] ``` 手寫實現一個函式模擬Object.entries? ```js function getObjectEntries(obj) { const result = []; for (const prop in obj) { if (obj.hasOwnProperty(prop)) { result.push([prop, obj[prop]]); } } return result; } console.log(getObjectEntries({ a: 1, b: 2 })) ``` 4. Object.getOwnPropertyNames 該方法返回一個數組,該陣列對元素是 obj自身擁有的列舉或不可列舉屬性名稱字串。 看一下這段程式碼會輸出什麼? ```js Object.prototype.aa = '1111'; const testData = { a: 1, b: 2 } for (const key in testData) { console.log(key); } console.log(Object.getOwnPropertyNames(testData)) ``` 5. Object.getOwnPropertyDescriptor 什麼是descriptor? 物件對應的屬性描述符, 是一個物件. 包含以下屬性: * configurable。 如果為false,則任何嘗試刪除目標屬性或修改屬性特性(writable, configurable, enumerable)的行為將被無效化。所以通常屬性都有特性時,可以把configurable設定為true即可。 * writable 是否可寫。設定成 false,則任何對該屬性改寫的操作都無效(但不會報錯,嚴格模式下會報錯),預設false。 * enumerable。是否能在for-in迴圈中遍歷出來或在Object.keys中列舉出來。 ```js const object1 = {}; Object.defineProperty(object1, 'p1', { value: 'lubai', writable: false }); object1.p1 = 'not lubai'; console.log(object1.p1); ``` 講到了defineProperty, 那麼肯定離不開Proxy. ```js // const obj = {}; // let val = undefined; // Object.defineProperty(obj, 'a', { // set: function (value) { // console.log(`${value} - xxxx`); // val = value; // }, // get: function () { // return val; // }, // configurable: true, // }) // obj.a = 111; // console.log(obj.a) const obj = new Proxy({}, { get: function (target, propKey, receiver) { console.log(`getting ${propKey}`); return target[propKey]; }, set: function (target, propKey, value, receiver) { console.log(`setting ${propKey}`); return Reflect.set(target, propKey, value, receiver); } }); obj.something = 1; console.log(obj.something); ``` Reflect又是個什麼東西? * 將Object物件的一些明顯屬於語言內部的方法(比如Object.defineProperty),放到Reflect物件上。現階段,某些方法同時在Object和Reflect物件上部署,未來的新方法將只部署在Reflect物件上。也就是說,從Reflect物件上可以拿到語言內部的方法 * 讓Object操作都變成函式行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函式行為。 * Reflect物件的方法與Proxy物件的方法一一對應,只要是Proxy物件的方法,就能在Reflect物件上找到對應的方法。這就讓Proxy物件可以方便地呼叫對應的Reflect方法,完成預設行為,作為修改行為的基礎。也就是說,不管Proxy怎麼修改預設行為,你總可以在Reflect上獲取預設行為。 但是要注意, 通過defineProperty設定writable為false的物件, 就不能用Proxy了 ```js const target = Object.defineProperties({}, { foo: { value: 123, writable: false, configurable: false }, }); const proxy = new Proxy(target, { get(target, propKey) { return 'abc'; } }); proxy.foo ``` 6. Object.create() Object.create()方法建立一個新的物件,並以方法的第一個引數作為新物件的__proto__屬性的值(根據已有的物件作為原型,建立新的物件。) Object.create()方法還有第二個可選引數,是一個物件,物件的每個屬性都會作為新物件的自身屬性,物件的屬性值以descriptor(Object.getOwnPropertyDescriptor(obj, 'key'))的形式出現,且enumerable預設為false ```js const person = { isHuman: false, printIntroduction: function () { console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`); } }; const me = Object.create(person); me.name = "lubai"; me.isHuman = true; me.printIntroduction(); console.log(person); const myObject = Object.create(null) ``` 傳入第二個引數是怎麼操作的呢? ```js function Person(name, sex) { this.name = name; this.sex = sex; } const b = Object.create(Person.prototype, { name: { value: 'coco', writable: true, configurable: true, enumerable: true, }, sex: { enumerable: true, get: function () { return 'hello sex' }, set: function (val) { console.log('set value:' + val) } } }) console.log(b.name) console.log(b.sex) ``` 那麼Object.create(null)的意義是什麼呢? 平時建立一個物件Object.create({}) 或者 直接宣告一個{} 不就夠了? Object.create(null)建立一個物件,但這個物件的原型鏈為null,即Fn.prototype = null ```js const b = Object.create(null) // 返回純{}物件,無prototype b // {} b.__proto__ // undefined b.toString() // throw error ``` 所以當你要建立一個非常乾淨的物件, 沒有任何原型鏈上的屬性, 那麼就使用Object.create(null). for in 遍歷的時候也不需要考慮原型鏈屬性了. 1. Object.assign 淺拷貝, 類似於 { ...a, ...b }; ```js function shallowClone(source) { const target = {}; for (const i in source) { if (source.hasOwnProperty(i)) { target[i] = source[i]; } } return target; } const a = { b: 1, c: { d: 111 } } const b = shallowClone(a); b.b = 2222; b.c.d = 333; console.log(b) console.log(a) ``` 8. Object.is ```js const a = { name: 1 }; const b = a; console.log(Object.is(a, b)) console.log(Object.is({}, {})) ``` ### Promise 大部分promise都講過了, 大概再來複習一下promise.all吧 ```js function PromiseAll(promiseArray) { return new Promise(function (resolve, reject) { //判斷引數型別 if (!Array.isArray(promiseArray)) { return reject(new TypeError('arguments muse be an array')) } let counter = 0; let promiseNum = promiseArray.length; let resolvedArray = []; for (let i = 0; i < promiseNum; i++) { // 3. 這裡為什麼要用Promise.resolve? Promise.resolve(promiseArray[i]).then((value) => { counter++; resolvedArray[i] = value; // 2. 這裡直接Push, 而不是用索引賦值, 有問題嗎 if (counter == promiseNum) { // 1. 這裡如果不計算counter++, 直接判斷resolvedArr.length === promiseNum, 會有問題嗎? // 4. 如果不在.then裡面, 而在外層判斷, 可以嗎? resolve(resolvedArray) } }).catch(e => reject(e)); } }) } // 測試 const pro1 = new Promise((res, rej) => { setTimeout(() => { res('1') }, 1000) }) const pro2 = new Promise((res, rej) => { setTimeout(() => { res('2') }, 2000) }) const pro3 = new Promise((res, rej) => { setTimeout(() => { res('3') }, 3000) }) const proAll = PromiseAll([pro1, pro2, pro3]) .then(res => console.log(res) // 3秒之後列印 ["1", "2", "3"] ) .catch((e) => { console.log(e) }) ``` 再來寫一個Promise.allSeettled, 需要返回所有promise的狀態和結果 ```js function PromiseAllSettled(promiseArray) { return new Promise(function (resolve, reject) { //判斷引數型別 if (!Array.isArray(promiseArray)) { return reject(new TypeError('arguments muse be an array')) } let counter = 0; const promiseNum = promiseArray.length; const resolvedArray = []; for (let i = 0; i < promiseNum; i++) { Promise.resolve(promiseArray[i]) .then((value) => { resolvedArray[i] = { status: 'fulfilled', value }; }) .catch(reason => { resolvedArray[i] = { status: 'rejected', reason }; }) .finally(() => { counter++; if (counter == promiseNum) { resolve(resolvedArray) } }) } }) } ``` ### 陣列 1. Array.flat flat() 方法會按照一個可指定的深度遞迴遍歷陣列,並將所有元素與遍歷到的子陣列中的元素合併為一個新陣列返回 ```js const arr1 = [1, 2, [3, 4]]; arr1.flat(); // [1, 2, 3, 4] const arr2 = [1, 2, [3, 4, [5, 6]]]; arr2.flat(); // [1, 2, 3, 4, [5, 6]] const arr3 = [1, 2, [3, 4, [5, 6]]]; arr3.flat(2); // [1, 2, 3, 4, 5, 6] //使用 Infinity,可展開任意深度的巢狀陣列 const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]; arr4.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ``` 如何模擬實現Array.flat? ```js // 使用 reduce、concat 和遞迴展開無限多層巢狀的陣列 const arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]]; function flatDeep(arr, d = 1) { if (d > 0) { return arr.reduce((res, val) => { if (Array.isArray(val)) { res = res.concat(flatDeep(val, d - 1)) } else { res = res.concat(val); } return res; }, []) } else { return arr.slice() } }; console.log(flatDeep(arr1, Infinity)) // [1, 2, 3, 1, 2, 3, 4, 2, 3, 4] ``` 如果不考慮深度, 咱們直接給他無限打平 ```js function flatten(arr) { let res = []; let length = arr.length; for (let i = 0; i < length; i++) { if (Object.prototype.toString.call(arr[i]) === '[object Array]') { res = res.concat(flatten(arr[i])) } else { res.push(arr[i]) } } return res } // 如果陣列元素都是Number型別 function flatten(arr) { return arr.toString().split(',').map(item => +item) } function flatten(arr){ while(arr.some(item=>Array.isArray(item))){ arr = [].concat(...arr); } return arr; } ``` 2. Array.includes includes() 方法用來判斷一個數組是否包含一個指定的值,根據情況,如果包含則返回 true,否則返回false。 ```js const array1 = [1, 2, 3]; console.log(array1.includes(2)); const pets = ['cat', 'dog', 'bat']; console.log(pets.includes('cat')); ``` 其實它有兩個引數, 只不過我們平時只使用一個. * valueToFind 需要查詢的元素值。 * fromIndex 可選 從fromIndex 索引處開始查詢 valueToFind。如果為負值,則按升序從 array.length + fromIndex 的索引開始搜 (即使從末尾開始往前跳 fromIndex 的絕對值個索引,然後往後搜尋)。預設為 0。 ```js [1, 2, 3].includes(2); // true [1, 2, 3].includes(4); // false [1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true [1, 2, NaN].includes(NaN); // true // fromIndex 大於等於陣列長度 var arr = ['a', 'b', 'c']; arr.includes('c', 3); // false arr.includes('c', 100); // false // 計算出的索引小於 0 var arr = ['a', 'b', 'c']; arr.includes('a', -100); // true arr.includes('b', -100); // true arr.includes('c', -100); // true ``` 3. Array.find find() 方法返回陣列中滿足提供的測試函式的第一個元素的值。否則返回 undefined。 callback 在陣列每一項上執行的函式,接收 3 個引數: * element 當前遍歷到的元素。 * index可選 當前遍歷到的索引。 * array可選 陣列本身。 ```js const test = [ {name: 'lubai', age: 11 }, {name: 'xxx', age: 100 }, {name: 'nnn', age: 50} ]; function findLubai(teacher) { return teacher.name === 'lubai'; } console.log(test.find(findLubai)); ``` 4. Array.from 4.1 Array.from() 方法從一個類似陣列或可迭代物件建立一個新的,淺拷貝的陣列例項。 * arrayLike 想要轉換成陣列的偽陣列物件或可迭代物件。 * mapFn 可選 如果指定了該引數,新陣列中的每個元素會執行該回調函式。 4.2 Array.from() 可以通過以下方式來建立陣列物件: * 偽陣列物件(擁有一個 length 屬性和若干索引屬性的任意物件) * 可迭代物件(可以獲取物件中的元素,如 Map和 Set 等) ```js console.log(Array.from('foo')); console.log(Array.from([1, 2, 3], x => x + x)); const set = new Set(['foo', 'bar', 'baz', 'foo']); Array.from(set); // [ "foo", "bar", "baz" ] const map = new Map([[1, 2], [2, 4], [4, 8]]); Array.from(map); // [[1, 2], [2, 4], [4, 8]] const mapper = new Map([['1', 'a'], ['2', 'b']]); Array.from(mapper.values()); // ['a', 'b']; Array.from(mapper.keys()); // ['1', '2']; ``` 所以陣列去重我們可以怎麼做? ```js function unique (arr) { return Array.from(new Set(arr)) // return [...new Set(arr)] } const test = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a']; console.log(unique(test)); function unique(arr) { const map = new Map(); const array = []; // 陣列用於返回結果 for (let i = 0; i < arr.length; i++) { if (!map.has(arr[i])) { // 如果有該key值 array.push(arr[i]); map.set(arr[i], true); } } return array; } function unique(arr) { if (!Array.isArray(arr)) { console.log('type error!') return } const array = []; for (let i = 0; i < arr.length; i++) { if (!array.includes(arr[i])) { //includes 檢測陣列是否有某個值 array.push(arr[i]); } } return array } ``` 5. Array.of Array.of() 方法建立一個具有可變數量引數的新陣列例項,而不考慮引數的數量或型別。 ```js Array.of(7); // [7] Array.of(1, 2, 3); // [1, 2, 3] ``` 那怎麼去模擬實現它呢? ```js Array.of = function() { return Array.prototype.slice.call(arguments); }; ``` ### async await yeild 「歡迎在評論區討論」 #### 希望看完的朋友可以給個贊,鼓勵一下