日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

什么?ES6 中还有 Tail Calls!

發(fā)布時(shí)間:2024/4/13 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 什么?ES6 中还有 Tail Calls! 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

前言

先吐槽一件事,最近把原先的 TOP 域名更換到 CN 域名,并且用 Gatsby 重建個(gè)人站點(diǎn),之前是用采用 HTTPS 部署的方式繞過阿里云的域名備案系統(tǒng)。更換 CN 域名后,這招不管用了,?? 域名必須要備案了,等待幕布郵寄中……

有人要問了,都 9102 年,ES10 都出來了,怎么還在講 ES6,非也!本文針對(duì) ES6 幾個(gè)不為人知、和重要的特性做講解,精彩的在后面!

基礎(chǔ)篇

Let + Const

ES6 除了固有的函數(shù)作用域,還引入了塊級(jí)作用域({})

function f() {{let x; // ①{// 包含在當(dāng)前塊中,與 ① 中的 x 分屬不同作用域const x = "sneaky";// 錯(cuò)誤,const 定義的變量不可以重新賦值,如果 const 定義了一個(gè)對(duì)象,那么對(duì)象的屬性是可以修改的x = "foo";}// let 定義的變量可以重新賦值x = "bar";// 錯(cuò)誤,x 在 ① 塊中已被定義let x = "inner";} } 復(fù)制代碼

默認(rèn)、剩余、展開參數(shù)(Default + Rest + Spread)

function f(x, y=12) {// y 等于 12 如果不傳遞 (或者傳遞 undefined)return x + y; } f(3); // 15 復(fù)制代碼function f(x, ...y) {// y 是一個(gè)數(shù)組return x * y.length; } f(3, "hello", true); // 6 復(fù)制代碼function f(x, y, z) {return x + y + z; } // 將數(shù)組的每一項(xiàng)作為參數(shù)傳遞 f(...[1,2,3]); // 6 復(fù)制代碼

解構(gòu)(Destructuring)

var [a, ,b] = [1,2,3]; a === 1; // true b === 3; // truevar { op: a, lhs: { op: b }, rhs: c } = getASTNode()// var {op: op, lhs: lhs, rhs: rhs} = getASTNode() var {op, lhs, rhs} = getASTNode() // 參數(shù)解構(gòu) function g({name: x}) {console.log(x); } g({name: 5})var [a] = []; a === undefined; // truevar [a = 1] = []; a === 1; // true// 解構(gòu) + 默認(rèn)參數(shù) function r({x, y, w = 10, h = 10}) {return x + y + w + h; } r({x:1, y:2}) === 23 // true 復(fù)制代碼

箭頭函數(shù)(Arrows and Lexical This)

// 除了支持返回語句,還可以將表達(dá)式作為返回主體 const foo = () => ({ name: 'es6' }); const bar = (num) => (num++, num ** 2);foo(); // 返回一個(gè)對(duì)象 { name: 'es6' } bar(3); // 執(zhí)行多個(gè)表達(dá)式,并返回最后一個(gè)表達(dá)式的值 16 復(fù)制代碼

JS 中 this 的指向問題一直都是面試高頻考點(diǎn),不少人在實(shí)戰(zhàn)中也掉入坑中,總結(jié)起來就是一句話:“ this 永遠(yuǎn)指向調(diào)用它的那個(gè)對(duì)象”,而箭頭函數(shù)則改寫了這一規(guī)則,就是

箭頭函數(shù)共享當(dāng)前代碼上下文的 this

什么意思呢?可以理解為

  • 箭頭函數(shù)不會(huì)創(chuàng)建自己的 this,它只會(huì)從自己的作用域鏈的上一層繼承 this,如果上一層還是箭頭函數(shù),則繼續(xù)向上查找,直至全局作用域,在瀏覽器環(huán)境下即 window。
  • 函數(shù)具有作用域鏈,對(duì)象則不具有

因此,在下面的代碼中,傳遞給 setInterval 的函數(shù)內(nèi)的 this 與 sayHello 函數(shù)中的 this 一致:

const bob = {name: 'Bob',sayHello() {setTimeout(() => {console.log(`hello, I am ${this.name}`);}, 1000);} }; const hello = bob.sayHello;bob.sayHello(); // hello, I am Bob // 作為對(duì)象的方法調(diào)用,sayHello的this指向bob hello(); // hello, I am undefined // 作為普通函數(shù)調(diào)用,相當(dāng)于window.hello(),this指向全局對(duì)象 hello.call({name:'Mike'}); // hello, I am Mike // call,apply調(diào)用,第一個(gè)參數(shù)為this指向的對(duì)象 復(fù)制代碼language = 'Python'; const obj = {language: 'TS',speak() {language = 'GO';return function() {return () => {console.log(`I speak ${this.language}`);};};} };obj.speak()()(); // 做個(gè)小測試,會(huì)打印什么呢? 復(fù)制代碼

箭頭函數(shù)還有以下特點(diǎn)

  • 由于箭頭函數(shù)沒有自己的 this 指針,通過 call 或 apply 調(diào)用,第一個(gè)參數(shù)會(huì)被忽略
  • 不綁定 Arguments 對(duì)象,其引用上一層作用域鏈的 Arguments 對(duì)象
  • 不能用作構(gòu)造器,和 new 一起用會(huì)拋出錯(cuò)誤。
  • 沒有 prototype 屬性。

現(xiàn)在你應(yīng)該明白為何 React 中的函數(shù)寫法都為箭頭函數(shù),就是為了綁定 this

Symbols

ES6 引入了一種新的原始數(shù)據(jù)類型 Symbol,表示獨(dú)一無二的值,它的功能類似于一種標(biāo)識(shí)唯一性的 ID

// 每個(gè) Symbol 實(shí)例都是唯一的。因此,當(dāng)你比較兩個(gè) Symbol 實(shí)例的時(shí)候,將總會(huì)返回 false const s1 = Symbol('macOS'); const s2 = Symbol('macOS');// Symbol.for 機(jī)制有點(diǎn)類似于單例模式 const s3 = Symbol.for('windows'); // 注冊一個(gè)全局 Symbol const s4 = Symbol.for('windows'); // 已存在相同名稱的 Symbol,返回全局 Symbols1 === s2; // false s3 === s4; // true復(fù)制代碼let key = Symbol('key');function MyClass(privateData) {// 注意,Symbol值作為對(duì)象屬性名時(shí),不能用點(diǎn)運(yùn)算符this[key] = privateData; }MyClass.prototype = {doStuff() {console.log(this[key]);} };// Symbol的一些特性必須要瀏覽器的原生實(shí)現(xiàn),不可被 transpiled 或 polyfilled typeof key // symbollet c = new MyClass('hello'); c.key; // undefined c[key]; // hello復(fù)制代碼

應(yīng)用場景

  • 更好的設(shè)計(jì)我們的數(shù)據(jù)對(duì)象,讓“對(duì)內(nèi)操作”和“對(duì)外選擇性輸出”變得更加優(yōu)雅。

在實(shí)際應(yīng)用中,我們經(jīng)常會(huì)需要使用 Object.keys() 或者 for...in 來枚舉對(duì)象的屬性名,那在這方面,Symbol 類型的 key 表現(xiàn)的會(huì)有什么不同之處呢?來看以下示例代碼:

let obj = {[Symbol('name')]: '一斤代碼',age: 18,title: 'Engineer' }Object.keys(obj) // ['age', 'title']for (let p in obj) {console.log(p) // 分別會(huì)輸出:'age' 和 'title' }Object.getOwnPropertyNames(obj) // ['age', 'title'] 復(fù)制代碼

也正因?yàn)檫@樣一個(gè)特性,當(dāng)使用 JSON.stringify() 將對(duì)象轉(zhuǎn)換成 JSON 字符串的時(shí)候,Symbol 屬性也會(huì)被排除在輸出內(nèi)容之外:

JSON.stringify(obj) // {"age":18,"title":"Engineer"} 復(fù)制代碼

由上代碼可知,Symbol 類型的 key 是不能通過 Object.keys() 或者 for...in 來枚舉的,所以,利用該特性,我們可以把一些不需要對(duì)外操作和訪問的屬性使用 Symbol 來定義。

  • 消除魔術(shù)字符串
const TYPE_AUDIO = 'AUDIO' const TYPE_VIDEO = 'VIDEO' const TYPE_IMAGE = 'IMAGE'function handleFileResource(resource) {switch(resource.type) {case TYPE_AUDIO:playAudio(resource)breakcase TYPE_VIDEO:playVideo(resource)breakcase TYPE_IMAGE:previewImage(resource)breakdefault:throw new Error('Unknown type of resource')} } 復(fù)制代碼

上面的代碼中那樣,我們需要為常量賦一個(gè)唯一的值(比如這里的 'AUDIO'),'AUDIO' 就是一個(gè)魔術(shù)字符串,它本身沒意義,只是為了保證常量唯一的關(guān)系。常量一多,就變得十分臃腫且難以理解

現(xiàn)在有了 Symbol,我們大可不必這么麻煩了:

// 保證了三個(gè)常量的值是唯一的 const TYPE_AUDIO = Symbol() const TYPE_VIDEO = Symbol() const TYPE_IMAGE = Symbol() 復(fù)制代碼

增強(qiáng)的對(duì)象字面量(Enhanced Object Literals)

const obj = {// 允許設(shè)置原型__proto__: theProtoObj,// 允許覆蓋屬性['__proto__']: somethingElse,// 屬性簡寫,等于 ‘handler: handler’handler,// 計(jì)算 (動(dòng)態(tài)) 屬性名['prop_' + (() => 42)()]: 42 }; obj.prop_42 // 42 obj.__proto__ // somethingElse 復(fù)制代碼

__proto__ 需要原生支持,它在之前的 ECMAScript 版本中被移除,但大多數(shù)瀏覽器都實(shí)現(xiàn)了這一特性,包括 Node 環(huán)境

Map + Set + WeakMap + WeakSet

Set

Set 是 ES6 中新增的數(shù)據(jù)結(jié)構(gòu),它允許創(chuàng)建唯一值的集合。集合中的值可以是簡單的基本類型(如字符串或數(shù)值),但更復(fù)雜的對(duì)象類型(如對(duì)象或數(shù)組)也可以,亦或是一個(gè)新的 Set

let animals = new Set();animals.add('?'); animals.add('?'); animals.add('?'); animals.add('?'); console.log(animals.size); // 4 animals.add('?'); console.log(animals.size); // 4console.log(animals.has('?')); // true animals.delete('?'); console.log(animals.has('?')); // falseanimals.forEach(animal => {console.log(`Hey ${animal}!`); });// Hey ?! // Hey ?! // Hey ?!animals.clear(); console.log(animals.size); // 0 復(fù)制代碼

我們還可以傳入一個(gè)數(shù)組來初始化集合

let myAnimals = new Set(['?', '?', '?', '?']);myAnimals.add(['?', '?']); myAnimals.add({ name: 'Rud', type: '?' }); console.log(myAnimals.size); // 4// Set 內(nèi)置了遍歷器,可以調(diào)用 forEach, for…of myAnimals.forEach(animal => {console.log(animal); });// ? // ? // ["?", "?"] // Object { name: "Rud", type: "?" } 復(fù)制代碼

Map

與普通對(duì)象(Object)不同,Map 的鍵名(Key)可以是任何類型,不再局限于字符串(String),包括但不限于 objects 或 functions

let things = new Map();const myFunc = () => '?';things.set('?', 'Car'); things.set('?', 'House'); things.set('??', 'Airplane'); things.set(myFunc, '? Key is a function!');things.size; // 4things.has('?'); // truethings.has(myFunc) // true things.has(() => '?'); // false things.get(myFunc); // '? Key is a function!'things.delete('??'); things.has('??'); // falsethings.clear(); things.size; // 0// 鏈?zhǔn)皆O(shè)置鍵值對(duì) things.set('?', 'Wrench').set('?', 'Guitar').set('?', 'Joystick');const myMap = new Map();// 甚至鍵名可以是另一個(gè) Map things.set(myMap, 'Oh gosh!'); things.size; // 4 things.get(myMap); // 'Oh gosh!'復(fù)制代碼

可以通過傳入包含兩個(gè)元素的數(shù)組來初始化 Map

const funArray = [['?', 'Champagne'],['?', 'Lollipop'],['?', 'Confetti'], ];let funMap = new Map(funArray); funMap.get('?'); // Champagne 復(fù)制代碼

WeakMap

WeakMap 對(duì)象是一組鍵/值對(duì)的集合,其中的鍵是弱引用的。其鍵必須是對(duì)象,而值可以是任意的。它最重要的特性是 WeakMap 保持了對(duì)鍵名所引用的對(duì)象的弱引用

我們可以通過 Node 來證明一下這個(gè)問題:

// 允許手動(dòng)執(zhí)行垃圾回收機(jī)制 node --expose-gcglobal.gc(); // 返回 Nodejs 的內(nèi)存占用情況,單位是 bytes process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4Mlet map = new Map(); let key = new Array(5 * 1024 * 1024); // new Array 當(dāng)為 Obj map.set(key, 1); global.gc(); process.memoryUsage(); // heapUsed: 46751472 注意這里大約是 44.6M// 所以當(dāng)你設(shè)置 key = null 時(shí),只是去掉了 key 對(duì) Obj 的強(qiáng)引用 // 并沒有去除 arr 對(duì) Obj 的強(qiáng)引用,所以 Obj 還是不會(huì)被回收掉 key = null; global.gc(); process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M// 這句話其實(shí)是無用的,因?yàn)?key 已經(jīng)是 null 了 map.delete(key); global.gc(); process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M 復(fù)制代碼node --expose-gcglobal.gc(); process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4Mconst wm = new WeakMap(); let key = new Array(5 * 1024 * 1024); wm.set(key, 1); global.gc(); process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M// 當(dāng)我們設(shè)置 key = null 的時(shí)候,就只有 wm 對(duì)所引用對(duì)象的弱引用 // 下次垃圾回收機(jī)制執(zhí)行的時(shí)候,該引用對(duì)象就會(huì)被回收掉。 key = null; global.gc(); process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M 復(fù)制代碼

應(yīng)用場景

傳統(tǒng)使用 jQuery 的時(shí)候,我們會(huì)通過 $.data() 方法在 DOM 對(duì)象上儲(chǔ)存相關(guān)信息(就比如在刪除按鈕元素上儲(chǔ)存帖子的 ID 信息),jQuery 內(nèi)部會(huì)使用一個(gè)對(duì)象管理 DOM 和對(duì)應(yīng)的數(shù)據(jù),當(dāng)你將 DOM 元素刪除,DOM 對(duì)象置為空的時(shí)候,相關(guān)聯(lián)的數(shù)據(jù)并不會(huì)被刪除,你必須手動(dòng)執(zhí)行 $.removeData() 方法才能刪除掉相關(guān)聯(lián)的數(shù)據(jù),WeakMap 就可以簡化這一操作:

let wm = new WeakMap(), element = document.querySelector(".element"); wm.set(element, "data");let value = wm.get(elemet); console.log(value); // dataelement.parentNode.removeChild(element); element = null; 復(fù)制代碼

WeakSet

特性與 WeakMap 相似

遍歷器(Iterators + For..Of)

遍歷器(Iterator)它是一種接口,為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機(jī)制。任何數(shù)據(jù)結(jié)構(gòu)只要部署 Iterator 接口,就可以完成遍歷操作(即依次處理該數(shù)據(jù)結(jié)構(gòu)的所有成員)。 Iterator 的作用有三個(gè):

  • 為各種數(shù)據(jù)結(jié)構(gòu),提供一個(gè)統(tǒng)一的、簡便的訪問接口;
  • 使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列;
  • ES6 創(chuàng)造了一種新的遍歷命令 for...of 循環(huán),Iterator 接口主要供 for...of 消費(fèi)。

ES6 規(guī)定,默認(rèn)的 Iterator 接口部署在數(shù)據(jù)結(jié)構(gòu)的 Symbol.iterator 屬性,或者說,一個(gè)數(shù)據(jù)結(jié)構(gòu)只要具有 Symbol.iterator 屬性,就可以認(rèn)為是“可遍歷的”(iterable)

let fibonacci = {[Symbol.iterator]() {let pre = 0, cur = 1;return {next() {[pre, cur] = [cur, pre + cur]; // 數(shù)組解構(gòu)return { done: false, value: cur }}}} }for (var n of fibonacci) {// 當(dāng)n超過1000時(shí)停止if (n > 1000)break;console.log(n); } 復(fù)制代碼

上面代碼中,對(duì)象 fibonacci 是可遍歷的(iterable),因?yàn)榫哂?Symbol.iterator 屬性。執(zhí)行這個(gè)屬性,會(huì)返回一個(gè)遍歷器對(duì)象。該對(duì)象的根本特征就是具有 next 方法。每次調(diào)用 next 方法,都會(huì)返回一個(gè)代表當(dāng)前成員的信息對(duì)象,具有 value 和 done 兩個(gè)屬性

原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)如下

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數(shù)的 arguments 對(duì)象
  • NodeList 對(duì)象

for...in 和 for..of 的差別

  • for...in 遍歷鍵名(Key)并轉(zhuǎn)化為字符串,for...of 遍歷鍵值(Value)
  • for...in 語句以任意順序遍歷一個(gè)對(duì)象自有的、繼承的、可枚舉的、非 Symbol 的屬性
  • for...in 更適合遍歷對(duì)象,for...of 更適合遍歷數(shù)組。
for (let i in [1, 2, 3]) {console.log(typeof i); // string 數(shù)組下標(biāo)被轉(zhuǎn)化字符串console.log(i); // '1', '2', '3' }var triangle = { a: 1, b: 2, c: 3 };function ColoredTriangle() {this.color = 'red'; }ColoredTriangle.prototype = triangle;var obj = new ColoredTriangle();for (var prop in obj) {if (obj.hasOwnProperty(prop)) { // 如果去了 hasOwnProperty() 這個(gè)約束條件會(huì)怎么樣?console.log(`obj.${prop} = ${obj[prop]}`); // obj.color = red} } 復(fù)制代碼

新增 API(Math + Number + String + Object APIs)

我們先來看看新增的 Number.EPSILON,不少人都是懵逼的狀態(tài),WTF?
先來看看的 JS 世界中的一道送命題

0.1 + 0.2 // 結(jié)果0.30000000000000004 而不是0.3 復(fù)制代碼

事出必有因,這是因?yàn)?JS 的數(shù)值采用了 IEEE 754 標(biāo)準(zhǔn),而且 JS 是弱類型語言,所以數(shù)字都是以64位雙精度浮點(diǎn)數(shù)據(jù)類型儲(chǔ)存。也就是說,JS 語言底層根本沒有整數(shù),所有數(shù)字都是小數(shù)!當(dāng)我們以為在用整數(shù)進(jìn)行計(jì)算時(shí),都會(huì)被轉(zhuǎn)換為小數(shù)

而浮點(diǎn)數(shù)都是以多位二進(jìn)制的方式進(jìn)行存儲(chǔ)的

十進(jìn)制的0.1用二進(jìn)制表示為:0.0 0011 0011 0011 0011…,循環(huán)部分是0011
十進(jìn)制0.2用二進(jìn)制表示為:0.0011 0011 0011 0011…,循環(huán)部分是0011

由于存儲(chǔ)空間有限,最后計(jì)算機(jī)會(huì)舍棄后面的數(shù)值,所以我們最后就只能得到一個(gè)近似值

JS中采用的 IEEE 754 的雙精度標(biāo)準(zhǔn)也是一樣的道理在存儲(chǔ)空間有限的情況下,當(dāng)出現(xiàn)這種無法整除的小數(shù)的時(shí)候就會(huì)取一個(gè)近似值,在 JS 中如果這個(gè)近似值足夠近似,那么 JS 就會(huì)認(rèn)為他就是那個(gè)值。

console.log(0.1000000000000001) // 0.1000000000000001 (中間14個(gè)0,不會(huì)被近似處理,輸出本身) console.log(0.10000000000000001) // 0.1 (中間15個(gè)0,js會(huì)認(rèn)為兩個(gè)值足夠近似,所以輸出0.1) 復(fù)制代碼

那么這個(gè)近似的界限如何判斷呢?

ES6的 Number.EPSILON就是一個(gè)界限,它表示 1 與大于 1 的最小浮點(diǎn)數(shù)之間的差。

對(duì)于 64 位浮點(diǎn)數(shù)來說,大于 1 的最小浮點(diǎn)數(shù)相當(dāng)于二進(jìn)制的1.00..001,小數(shù)點(diǎn)后面有連續(xù) 51 個(gè)零。這個(gè)值減去 1 之后,就等于 2 的 -52 次方

Number.EPSILON === Math.pow(2, -52) // true Number.EPSILON // 2.220446049250313e-16 Number.EPSILON.toFixed(20) // "0.00000000000000022204" 復(fù)制代碼

Number.EPSILON 實(shí)際上是 JavaScript 能夠表示的最小精度。誤差如果小于這個(gè)值,就可以認(rèn)為已經(jīng)沒有意義了,即不存在誤差了。

0.1 + 0.2 - 0.3 // 5.551115123125783e-175.551115123125783e-17.toFixed(20) // '0.00000000000000005551' 復(fù)制代碼0.00000000000000005551 < 0.00000000000000022204 // true 復(fù)制代碼

顯然,0.30000000000000004 不存在誤差,不會(huì)被近似處理

我們可以通過以下手段來達(dá)到我們想要的效果

function withinErrorMargin (left, right) {return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2); }0.1 + 0.2 === 0.3 // false withinErrorMargin(0.1 + 0.2, 0.3) // true 復(fù)制代碼

其他一些新增的 API

Number.isInteger(Infinity) // false Number.isNaN("NaN") // falseMath.sign(-5) // 判斷一個(gè)數(shù)到底是正數(shù)、負(fù)數(shù)、還是零 -1 Math.hypot(3, 4) // 返回所有參數(shù)的平方和的平方根 5 Math.imul(-2, -2) // 返回兩個(gè)數(shù)以 32 位帶符號(hào)整數(shù)形式相乘的結(jié)果 4"abcde".includes("cd") // true "abc".repeat(3) // "abcabcabc"Array.from(document.querySelectorAll("*")) // 返回一個(gè)真正的數(shù)組 Array.of(1, 2, 3) // [1,2,3] [0, 0, 0].fill(7, 1) // [0,7,7] [1,2,3].findIndex(x => x == 2) // 1 ["a", "b", "c"].entries() // [0, "a"], [1,"b"], [2,"c"] ["a", "b", "c"].keys() // 0, 1, 2 ["a", "b", "c"].values() // "a", "b", "c"Object.assign(Point, { origin: new Point(0,0) }) // 合并對(duì)象 復(fù)制代碼

二進(jìn)制和八進(jìn)制字面量(Binary and Octal Literals)

0b111 === 7 // true 二進(jìn)制 0o111 === 73 // true 八進(jìn)制 0x111 === 273 // true 十六進(jìn)制 復(fù)制代碼

進(jìn)階篇

尾遞歸(Tail Calls)

假設(shè)現(xiàn)在要實(shí)現(xiàn)一個(gè)階乘函數(shù),即 5!= 120,我們很容易想到遞歸實(shí)現(xiàn)

function factorial(n) {if (n === 1) return 1;return n * factorial(n - 1); } 復(fù)制代碼

但遞歸非常耗費(fèi)內(nèi)存,因?yàn)樾枰瑫r(shí)保存成千上百個(gè)調(diào)用記錄,很容易發(fā)生"棧溢出"錯(cuò)誤(stack overflow)。但對(duì)于尾遞歸來說,由于只存在一個(gè)調(diào)用記錄,所以永遠(yuǎn)不會(huì)發(fā)生"棧溢出"錯(cuò)誤。

何為調(diào)用記錄,在示例代碼中,由于最后一步返回了一個(gè)表達(dá)式,內(nèi)存會(huì)保留 n 這個(gè)變量的信息和 factorial(n - 1) 調(diào)用下一次函數(shù)的位置,形成一層層的調(diào)用棧

尾遞歸的實(shí)現(xiàn),往往需要改寫遞歸函數(shù),確保最后一步只調(diào)用自身,返回函數(shù)本身。做到這一點(diǎn)的方法,就是把所有用到的內(nèi)部變量改寫成函數(shù)的參數(shù)。尾遞歸優(yōu)化如下

function factorial(n, acc = 1) { "use strict";if (n <= 1) return acc;return factorial(n - 1, n * acc); }factorial(100000) 復(fù)制代碼

由此可見,"尾調(diào)用優(yōu)化"對(duì)遞歸操作意義重大,所以一些函數(shù)式編程語言將其寫入了語言規(guī)格。ES6 也是如此,第一次明確規(guī)定,所有 ECMAScript 的實(shí)現(xiàn),都必須部署"尾調(diào)用優(yōu)化"。這就是說,在 ES6 中,只要使用尾遞歸,就不會(huì)發(fā)生棧溢出,相對(duì)節(jié)省內(nèi)存。

ES6的尾調(diào)用優(yōu)化只在嚴(yán)格模式下開啟,正常模式是無效的。

反射(Reflect)

Reflect 對(duì)象與 Proxy 對(duì)象一樣,也是 ES6 為了操作對(duì)象而提供的新 API。Reflect 對(duì)象的設(shè)計(jì)目的有這樣幾個(gè)。

  • 將Object對(duì)象的一些明顯屬于語言內(nèi)部的方法(比如 Object.defineProperty),放到 Reflect 對(duì)象上
  • 修改某些 Object 方法的返回結(jié)果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc) 在無法定義屬性時(shí),會(huì)拋出一個(gè)錯(cuò)誤,而 Reflect.defineProperty(obj, name, desc) 則會(huì)返回 false。
var O = {a: 1}; Reflect.defineProperty(O, 'b', {value: 2}); O[Symbol('c')] = 3;Reflect.ownKeys(O); // ['a', 'b', Symbol(c)] Reflect.getOwnPropertyDescriptor(O, 'b'); // { value: 2, writable: false, enumerable: false, configurable: false } function C(a, b){this.c = a + b; } var instance = Reflect.construct(C, [20, 22]); instance.c; // 42 復(fù)制代碼

獲取屬性名的方法有很多,以上面的代碼為例子,它們的區(qū)別如下

方法結(jié)果解釋
Object.getOwnPropertyNames(O)[ 'a', 'b' ]獲取除 Symbol 外的所有屬性
Object.getOwnPropertySymbols(O)[ Symbol(c) ]只獲取 Symbol 屬性
OReflect.ownKeys(O)[ 'a', 'b', Symbol(c) ]獲取所有屬性
for...ina獲取除 Symbol 外的可枚舉屬性

代理(Proxy)

Proxy 可以理解成,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問,都必須先通過這層攔截,因此提供了一種機(jī)制,可以對(duì)外界的訪問進(jìn)行過濾和改寫。Proxy 這個(gè)詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。

// 代理一個(gè)對(duì)象 var target = {}; var handler = {get: function (receiver, name) {return `Hello, ${name}!`;} };var p = new Proxy(target, handler); p.world; // "Hello, world!" 復(fù)制代碼// 代理一個(gè)函數(shù) var target = function () { return "I am the target"; }; var handler = {apply: function (receiver, ...args) {return "I am the proxy";} };var p = new Proxy(target, handler); p(); // "I am the proxy" 復(fù)制代碼// 代理會(huì)將所有應(yīng)用到它的操作轉(zhuǎn)發(fā)到這個(gè)對(duì)象上 let target = {}; let p = new Proxy(target, {});p.a = 37; target.a; // 37 操作轉(zhuǎn)發(fā)到目標(biāo) 復(fù)制代碼// 如何實(shí)現(xiàn) a == 1 && a == 2 && a == 3,利用Proxy的get劫持 const a = new Proxy({},{val: 1,get() {return () => this.val++;}} ); a == 1 && a == 2 && a == 3; // true 復(fù)制代碼

由于 ES5 的限制,Proxy 不能被 transpiled or polyfilled,自己親自入的坑,由于在項(xiàng)目中使用了 Mobx5.x,其內(nèi)部是用 Proxy 寫的,結(jié)果 IE11 不支持 ES6,只得回退版本 Mobx 到 4.x

生成器(Generators)

Generator 函數(shù)是 ES6 提供的一種異步編程解決方案。 Generator 函數(shù)有多種理解角度。語法上,首先可以把它理解成,Generator 函數(shù)是一個(gè)狀態(tài)機(jī),封裝了多個(gè)內(nèi)部狀態(tài)。

形式上,Generator 函數(shù)是一個(gè)普通函數(shù),但是有兩個(gè)特征。一是,function 關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào);二是,函數(shù)體內(nèi)部使用 yield 表達(dá)式,定義不同的內(nèi)部狀態(tài)

function* helloWorldGenerator() {yield 'hello'; // yield使Generator函數(shù)暫停了執(zhí)行,并將結(jié)果返回給調(diào)用者yield 'world'; // 當(dāng)下一次調(diào)用時(shí),從它中斷的地方恢復(fù)執(zhí)行return 'ending'; }var hw = helloWorldGenerator(); a = hw.next(); // { value: 'hello', done: false } b = hw.next(); // { value: 'world', done: false } c = hw.next(); // { value: 'ending', done: true } 復(fù)制代碼

可以利用這種暫停執(zhí)行的特性,來實(shí)現(xiàn)惰性求值

向Generator傳遞數(shù)據(jù)

function* sayFullName() {const firstName = yield;const secondName = yield;console.log(firstName + ' ' + secondName); } let fullName = sayFullName(); fullName.next(); // 第一次調(diào)用,代碼暫停在 const firstName = yield,因?yàn)闆]有通過 yield 發(fā)送任何值,因此 next 將返回 undefined fullName.next('Handsome'); // 第二次調(diào)用,傳入了值 Handsome,yield 被 Handsome 替代,因此 firstName 的值變?yōu)?Handsome,代碼執(zhí)行恢復(fù) // 直到再次遇到 const secondName = yield 暫停執(zhí)行 fullName.next('Jack'); // 第三次調(diào)用,傳入了值 Jack,yield 被 Jack 替代,因此 secondName 的值變?yōu)?Jack,代碼執(zhí)行恢復(fù) // 打印 Handsome Jack 復(fù)制代碼

使用Generator處理異步調(diào)用

let generator; let getDataOne = () => {setTimeout(() => {generator.next('dummy data one');}, 1000); }; let getDataTwo = () => {setTimeout(() => {generator.next('dummy data one');}, 1000); };function* main() {let dataOne = yield getDataOne();let dataTwo = yield getDataTwo();console.log(dataOne, dataTwo); } generator = main(); generator.next(); // 執(zhí)行 getDataOne(),然后 yield 暫停 // 直至一秒后 generator.next('dummy data one') 恢復(fù)代碼執(zhí)行,并賦值 dataOne console.log('i am previous print'); // i am previous print // dummy data one dummy data one 復(fù)制代碼

Promises

Promises 是一個(gè)異步編程的解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大。

所謂 Promise,簡單說就是一個(gè)容器,里面保存著某個(gè)未來才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。從語法上說,Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。

function timeout(duration = 0) {return new Promise((resolve, reject) => {setTimeout(resolve, duration);}) }var p = timeout(1000).then(() => {return timeout(2000); }).then(() => {throw new Error("hmm"); }).catch(err => {return Promise.all([timeout(100), timeout(200)]); }) 復(fù)制代碼

這里強(qiáng)調(diào)幾點(diǎn)

  • 不要?jiǎng)儕Z函數(shù) return 的能力,很多人寫 Promise,照樣有大量嵌套,掉進(jìn) Promise 地獄,要記得及時(shí) return,避免嵌套
  • 當(dāng)需要多個(gè)請求全部結(jié)束時(shí),才更新數(shù)據(jù),可以用 Promise.all(fetch1,fetch2)
  • 當(dāng)需要從多個(gè)請求中,接受最先返回?cái)?shù)據(jù)的那個(gè)請求,可以用 Promise.race(fetch1,fetch2)

結(jié)尾

ES6 是 ECMAScript 一個(gè)非常重要的版本,我們必須深入理解,不僅能提高我們書寫代碼的能力,還能增強(qiáng)業(yè)務(wù)能力

附上一張我之前精心整理的思維導(dǎo)圖

本文參考資料

  • Learn ES2015
  • ECMAScript 6 入門
  • ES6 系列之 WeakMap

轉(zhuǎn)載于:https://juejin.im/post/5cf0ebc26fb9a07ee27afc60

總結(jié)

以上是生活随笔為你收集整理的什么?ES6 中还有 Tail Calls!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。