做者 | 司徒正美 責編 | 郭芮 出品 | CSDN(ID:CSDNnews) JavaScript能發展到如今的程度已經經歷很多的坎坷,早產帶來的某些缺陷是永久性的,所以瀏覽器纔有禁用JavaScript的選項。甚至在jQuery時代有人問出這樣的問題,jQuery與JavaScript哪一個快?在Babel.js出來以前,發明一門全新的語言代碼代替JavaScript的呼聲一直不絕於耳,前有VBScript,Coffee, 後有Dartjs, WebAssembly。要不是它是全部瀏覽器都內置的腳本語言, 可能就命絕於此。瀏覽器就是它的那個有錢的丈母孃。此外源源不斷的類庫框架,則是它的武器庫,從底層革新了它本身。爲何這麼說呢? JavaScript沒有其餘語言那樣龐大的SDK,針對某一個領域自帶的方法是不多,好比說數組方法,字符串方法,都不超過20個,是Prototype.js給它加上的。JavaScript要實現頁面動效,離不開DOM與BOM,但瀏覽器互相競爭,致使API不一致,是jQuery搞定了,還帶來了鏈式調用與IIFE這些新的編程技巧。在它缺少大規模編程模式的時候,其餘語言的外來戶又給它帶來了MVC與MVVM……這裏面許多東西,長此以往都變成語言內置的特性,好比Prototype.js帶來的原型方法,jQuery帶來的選擇器方法,實現MVVM不可缺乏的對象屬性內省機制(getter, setter, Reflect, Proxy), 大規模編程須要的class, modules。 本文將如下幾個方面介紹這些新特性,正是它們武裝了JavaScript,讓它變成一個正統的,魔幻的語言。 原型方法的極大豐富;javascript
類與模塊的標準化;html
異步機制的嬗變;前端
塊級做用域的補完;vue
基礎類型的增長;java
反射機制的完善;node
更順手的語法糖。jquery
原型方法的極大豐富 原型方法自Prototype.js出來後,就不斷被招安成官方API。基本上在字符串與數組這兩大類別擴充,它們在平常業務中不斷被使用,所以不斷變重複造輪子,所以亟待官方化。webpack
JavaScript的版本說明:git
這些原型方法很是有用,以至於在面試中常常被問到,若是去除字符串兩邊的空白,如何扁平化一個數組?es6
類與模塊的標準化 在沒有類的時代,每一個流行框架都會帶一個建立類的方法,可見你們都不太認同原型這種複用機制。 下面是原型與類的寫法比較:
function Person(name) { this.name = name; } //定義一個方法而且賦值給構造函數的原型 Person.prototype.sayName = function () { return this.name; };
var p = new Person('ruby'); console.log(p.sayName()) // ruby
class Person { constructor(name){ this.name = name } sayName() { return this.name; } } var p = new Person('ruby'); console.log(p.sayName()) // ruby 咱們能夠看到es6的定義是很是簡單的,而且不一樣於對象鍵值定義方式,它是使用對象簡寫來描述方法。若是是標準的對象描述法,應該是這樣:
//下面這種寫法並不合法 class Person { constructor: function(name){ this.name = name } sayName: function() { return this.name; } } 若是咱們想繼承一個父類,也很簡單:
class Person extends Animal { constructor: function(name){ super(); this.name = name } sayName: function() { return this.name; } } 此外,它後面還補充了三次相關的語法,分別是屬性初始化語法,靜態屬性與方法語法,私有屬性語法。目前私有屬性語法爭議很是大,但仍是被標準化。雖然像typescript的private、public、protected更符合從後端轉行過來的人的口味,不過在babel無所不能的今天,咱們徹底可使用本身喜歡的寫法。 與類一塊兒出現的還有模塊,這是一種比類更大的複用單元,以文件爲載體,能夠實現按需加載。固然它最主要的做用是減小全局污染。jQuery時代,經過IIFE減小了這症狀,可是JS文件沒有統一的編寫規範,意味着想把它們打包一個是很是困難的,只能像下面那樣平鋪着。這些文件的依賴關係,只有最初的人知道,要了幾輪開發後,就是定時炸彈。此外,不要忘記,
因而後jQuery時代,國內流行三種模塊機制,以seajs主體的CMD,以requirejs爲主體的AMD,及nodejs自帶的Commonjs。固然,後來還有一種三合一方案UMD(AMD, Commonjs與es6 modules)。 requirejs的定義與使用:
define(['jquery'], function($){ //some code var mod = require("./relative/name"); return { //some code } //返回值能夠是對象、函數等 })
require(['cores/cores1', 'cores/cores2', 'utils/utils1', 'utils/utils2'], function(cores1, cores2, utils1, utils2){ //some code }) requirejs是世界第一款通用的模塊加載器,尤爲自創了shim機制,讓許多不模範的JS文件也能夠歸入其加載系統。
define(function(require){ var ("#container").html("hello,seajs"); var service = require("./service") var s = new service(); s.hello(); }); //另外一個獨立的文件service.js define(function(require,exports,module){ function Service(){ console.log("this is service module"); } Service.prototype.hello = function(){ console.log("this is hello service"); return this; } module.exports = Service; }); Seajs是阿里大牛玉伯加的加載器,借鑑了Requiejs的許多功能,據說其性能與嚴謹性超過前者。當前爲了正確分析出define回調裏面的require語句,還發起了一個 100 美刀賞金活動,讓國內高手一展身手。 github.com/seajs/seajs…
image_1doan2vfl17ld1nin1hbm182c9b9p.png-72.9kB 相對而言,nodejs模塊系統就簡單多了,它沒有專門用於包裹用戶代碼的define方法,它不須要顯式聲明依賴。
//world.js exports.world = function() { console.log('Hello World'); } //main.js let world = require('./world.js') world(); function Hello() { var name; this.setName = function(thyName) { name = thyName; }; this.sayHello = function() { console.log('Hello ' + name); }; }; module.exports = Hello; 而官方欽點的es6 modules與nodejs模塊系統極其類似,只是將其方法與對象變成關鍵字。
//test.js或test.mjs import * as test from './test'; //aaa.js或aaa.mjs import {aaa} from "./aaa" const arr = [1, 2, 3, 4]; const obj = { a: 0, b: function() {} } export const foo = () => { const a = 0; const b = 20; return a + b; } export default { num, arr, obj, foo } 那怎麼使用呢?根據規範,瀏覽器須要在link標籤與script標籤添加新的屬性或屬性值來支持這新特性。(詳見:www.jianshu.com/p/f7db50cf9…
但惋惜的是,瀏覽器對模塊系統的支持是很是滯後,而且即使最新的瀏覽器支持了,咱們仍是免不了要兼容舊的瀏覽器。對此,咱們只能奠出webpack這利器,它是前端工程化的集大成者,能夠將咱們的代碼經過各類loader/plugin打包成主流瀏覽器都認識的JavaScript語法,並以最原始的方式掛載進去。異步機制的嬗變 在JavaScript沒有大規模應用前,用到異步的地方只有ajax請求與動畫,在請求結束與動畫結束時要作什麼事,使用的辦法是經典的回調。 回調 因爲javascript是單線程的,咱們的方法是同步的,像下面這樣,一個個執行:
A(); B(); C(); 而異步則是不可預測其觸發時機:
A(); // 在如今發送請求 ajax({ url: url, data: {}, success:function(res){ // 在將來某個時刻執行 B(res) } }) C(); //執行順序:A -> C -> B 回調函數是主函數的後繼方法,基本上能保證,主函數執行後,它能在以後某個時刻被執行一次。但隨着功能的細分,在微信小程序或快應用中,它們拆分紅三個,即一個方法跟着三個回調。
// doc.quickapp.cn/features/sy… import share from '@system.share' share.share({ type: 'text/html', data: 'bold', success: function(){}, fail: function(){}, complete: function(){} }) 在nodejs中,內置的異步方法都是使用一種叫Error-first回調模式。
fs.readFile('/foo.txt', function(err, data) { // TODO: Error Handling Still Needed! console.log(data); }); 在後端,因爲存在IO操做,異步操做很是多,異步套異步很容易形成回調地獄。因而出現了另外一種模式,事件中心,EventBus或EventEmiiter。
var EventEmitter = require('events').EventEmitter; var ee = new EventEmitter(); ee.on('some_events', function(foo, bar) { console.log("第1個監聽事件,參數foo=" + foo + ",bar="+bar ); }); console.log('第一輪'); ee.emit('some_events', 'Wilson', 'Zhong'); console.log('第二輪'); ee.emit('some_events', 'Wilson', 'Z'); 事件能夠一次綁定,屢次觸發,而且能夠將原來內部的回調拖出來,有效地避免了回調地獄。但事件中心,對於同一種行爲,老是解發一種回調,不能像小程序的回調那麼清晰。因而jQuery引進了Promise。 Promise Promise最初叫Deffered,從Python的Twisted框架中引進過來。它經過異步方式完成用類的構建,又經過鏈式調用解決了回調地獄問題。
var p = new Promise(function(resolve, reject){ console.log("========") setTimeout(function(){ resolve(1) },300) setTimeout(function(){ //reject與resolve只能二選一 reject(1) },400) }); console.log("這個先執行") p.then(function (result) { console.log('成功:' + result); }) .catch(function (reason) { console.log('失敗:' + reason); }).finally(function(){ console.log("總會執行") }) 爲何這麼說呢?看上面的示例,new Promise(executor)裏的executor方法,它會待到then, catch, finally等方法添加完,纔會執行,它是異步的。而then, catch, finally則又剛好對應success, fail, complete這三種回調,咱們能夠爲Promise以鏈式方式添加多個then方法。 若是你不想寫catch,新銳的瀏覽器還提供了一個新事件作統一處理:
window.addEventListener('unhandledrejection', function(event) { // the event object has two special properties: alert(event.promise); // [object Promise] - 產生錯誤的 promise alert(event.reason); // Error: Whoops! - 未處理的錯誤對象 });
new Promise(function() { throw new Error("Whoops!"); }); // 沒有 catch 處理錯誤 nodejs也有相同的事件:
process.on('unhandledRejection', (reason, promise) => { console.log('未處理的拒絕:', promise, '緣由:', reason); // 記錄日誌、拋出錯誤、或其餘邏輯。 }); 除此以外,esma2020年還爲Promise添加了三個靜態方法:Promise.all()和Promise.race(),Promise.allSettled() 。 其實chrome 60已經均可以用了。 Promise.all(iterable) 方法返回一個 Promise 實例,此實例在 iterable 參數內全部的 promise 都「完成(resolved)」或參數中不包含 promise 時回調完成(resolve);若是參數中 promise 有一個失敗(rejected),此實例回調失敗(reject),失敗緣由的是第一個失敗 promise 的結果。
var promise1 = Promise.resolve(3); var promise2 = 42; var promise3 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, 'foo'); });
Promise.all([promise1, promise2, promise3]).then(function(values) { console.log(values); }); // expected output: Array [3, 42, "foo"] 這個方法相似於jQuery.when,專門用於處理併發事務。 Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。此方法用於競態的狀況。 Promise.allSettled(iterable)方法返回一個promise,該promise在全部給定的promise已被解析或被拒絕後解析,而且每一個對象都描述每一個promise的結果。它相似於Promise.all,但不會由於一個reject就會執行後繼回調,必須全部promise都被執行纔會。 Promise不併比EventBus, 回調等優異,可是它給前端API提供了一個標槓,之後處理異步就是返回一個Promise。爲後來async/await作了鋪墊。 生成器 生成器generator, 不是爲解決異步問題而誕生的,只是剛好它的某個特性能夠解耦異步的複雜性,加之koa的暴紅,人們發現原來generator還能夠這樣用,因而就火了。 爲了理解生成器的含義,咱們須要先了解迭代器,迭代器中的迭代就是循環的意思。好比es5中的forEach, map, filter就是迭代器。
let numbers = [1, 2, 3]; for (let i = 0; i < numbers.length; i++) { console.log(numbers[i]); } //它比上面更精簡 numbers.forEach(function(el){ console.log(el); }) 但forEach會一會兒把全部元素都遍歷出來,而咱們喜歡一個個處理呢?那咱們就要手寫一個迭代器。
function makeIterator(array){ var nextIndex = 0; return { next: function(){ return nextIndex < array.length ? {value: array[nextIndex++], done: false} : {done: true}; } }; }
var it = makeIterator([1,2,3]) console.log(it.next()); // {value: 1, done: false} console.log(it.next()); // {value: 2, done: false} console.log(it.next()); // {value: 3, done: false} console.log(it.next()); // {done: true} 而生成器則將建立迭代器經常使用的模式官方化,就像建立類同樣,可是它寫法有點怪,不像類那樣專門弄一個關鍵字,也沒有像Promise那樣弄一個類。
//理想中是這樣的 Iterator{ exector(){ yield 1; yield 2; yield 3; } } //現實是這樣的 function* Iterator() { yield 1; yield 2; yield 3; } 其實最好是像Promise那樣,弄一個類,那麼咱們還能夠用現成的語法來模擬,但生成器,如今一個新關鍵字yield,你能夠將它當一個return語句。生成器執行後,會產生一個對象,它有一個next方法,next方法執行多少次,就輪到第幾個yield的值返回。
function* Iterator() { yield 1; yield 2; yield 3; } let it = Iterator(); console.log(it.next()); // {value: 1, done: false} console.log(it.next()); // {value: 2, done: false} console.log(it.next()); // {value: 3, done: false} console.log(it.next()); // {value: undefined, done: true} 因爲寫法比較離經背道,所以一般見於類庫框架,業務中不多有人使用。它涉及許多細節,好比說yield與return的混用。
function* generator() { yield 1; return 2; //這個被轉換成 yield 2, 並當即設置成done: true yield 3; //這個被忽略 }
let it = generator(); console.log(it.next()); // {value: 1, done: false} console.log(it.next()); // {value: 2, done: true} console.log(it.next()); // {value: undefined, done: true}
image_1doda17jkj7kl4u1qru1era2m316.png-322.9kB 但說了這麼多,這與異步有什麼關係呢?咱們之因此須要回調,事件,Promise這些,實際上是但願能實現以同步代碼的方式組件異步邏輯。yield至關一個斷點,能中斷程序往下執行。因而異步的邏輯就能夠這樣寫:
function* generator() { yield setTimeout(function(){ console.log("111"), 200}) yield setTimeout(function(){ console.log("222"), 100}) } let it = generator(); console.log(it.next()); // 1 視瀏覽器有所差別 console.log(it.next()); // 2 視瀏覽器有所差別 若是沒有yield,確定是先打出222,再打出111。 好了,咱們搞定異步代碼以同步代碼的順序輸出後,就處理手動執行next方法的問題。這個也簡單,寫一個方法,用程序執行它們。
function timeout(data, time){ return new Promise(function(resolve){ setTimeout(function(){ console.log(data, new Date - 0) resolve(data) },time) }) } function generator(){ let p1 = yield timeout(1, 2000) console.log(p1) let p2 = yield timeout(2, 3000) console.log(p2) let p3 = yield timeout(3, 2000) console.log(p3) return 2; } // 按順序輸出 1 2 3 / 傳入要執行的gen / / 其實循環遍歷全部的yeild (函數的遞歸) 根絕next返回值中的done判斷是否執行到最後一個, 若是是最後一個則跳出去*/ function run(fn) { var gen = fn(); function next(data) { // 執行gen.next 初始data爲undefined var result = gen.next(data) // 若是result.done 爲true if(result.done) { return result.value }else{ // result.value 爲promise result.value.then(val=>{ next(val) }) } } // 調用上一個next方法 next(); } run(generator) koa早些年的版本依賴的co庫,就是基於上述原理擺平異步問題。有興趣的同窗能夠下來看看。 async/await 上節章的生成器已經完美地解決異步的邏輯以同步的代碼編寫的問題了,什麼異常,能夠直接try catch,成功則直接往下走,老是執行能夠加finally語句,美中不足是須要對yield後的方法作些改造,改爲Promise(這個也有庫,在nodejs直接內置了util.promisefy)。而後須要一個run方法,代替手動next。因而處於語言供應鏈上流的大佬們想,能不能直接將這兩步內置呢?而後包裝一個已經被人接受的語法提供給沒有見過世面的前端工程師呢?他們搜刮了一遍,還真有這東西。那就是C#有async/await。
//C# 代碼 public static async Task AddAsync(int n, int m) { int val = await Task.Run(() => Add(n, m)); return val; } 這種沒有學習成本的語法很快遷移到JS中,async關鍵字,至關於生成器函數與咱們自造的執行函數,await關鍵字至關於yield,但它只有在它跟着的是Promise纔會中斷流程執行。async函數最後會返回一個Promise,能夠供外面的await關鍵字使用。
//javascript 代碼 async function addTask() { await new Promise(function(resolve){ setTimeout(function(){ console.log("111"); resolve(), 200}) }) console.log('222') await new Promise(function(resolve){ setTimeout(function(){ console.log("333"); resolve(), 200}) }) console.log('444') } var p = addTask() console.log(p)
image_1dodd79nc1imnnm91q1b1p7qhdp1j.png-6.1kB 在循環中使用async/await:
const array = ["a","b", "c"] function getNum(num){ return new Promise(function(resolve){ setTimeout(function(){ resolve(num) }, 300) }) } async function asyncLoop() { console.log("start") for(let i = 0; i < array.length; i++){ const num = await getNum(array[i]); console.log(num, new Date-0) } console.log("end") } asyncLoop() async函數裏面的錯誤也能夠用try catch包住,也可使用上面提到的unhandledrejection方法。
async function addTask() { try{ await ... console.log('222') }catch(e){ console.log(e) } } 此外,es2018還添加了異步迭代器與異步生成器函數,讓咱們處理各類異步場景更加駕輕就熟:
//異步迭代器 const ruby = { [Symbol.asyncIterator]: () => { const items = [r
, u
, b
, y
, l
, o
,u
, v
, r
, e
]; return { next: () => Promise.resolve({ done: items.length === 0, value: items.shift() }) } } } for await (const item of ruby) { console.log(item) } //異步生成器函數,async函數與生成器函數的混合體 async function* readLines(path) { let file = await fileOpen(path);
try { while (!file.EOF) { yield await file.readLine(); } } finally { await file.close(); } }
塊級做用域的補完
提及做用域,你們通常認爲JavaScript只有全局做用域與函數做用域,可是es3時代,它仍是能經過catch語句與with語句創造塊級做用域的。
try{ var name = 'global' //全局做用域 }catch(e){ var b = "xxx" console.log(b)//xxx } console.log(b) var obj = { name: "block" } with(obj) { console.log(name);//Block塊上的name block } console.log(name)//global 可是catch語句執行後,仍是會污染外面的做用域,而且catch是很耗性能的。而with更不用說了,會引發歧義,被es5嚴格模式禁止了。 話又說回來,之因此須要塊狀做用域,是用來解決es3的兩個很差的設計,一個是變量提高,一個重複定義,它們都不利於團隊協做與大規模生產。
var x = 1; function rain(){ alert( x ); //彈出 'undefined',而不是1 var x = 'rain-man'; alert( x ); //彈出 'rain-man' } rain(); 所以到es6中,新添了let和const關鍵字來實現塊級做用域。這兩個關鍵字相比var,有以下特色: 做用域是局部,做用範圍是括起它的兩個花括號間,即for(){}, while(){}, if(){}與單純的{}。 它也不會提高到做用域頂部,它頂部到定義的那一行變稱之爲「暫時性死區」,這時使用它會報錯。 變量一旦變let, const聲明,就再不能重複定義,不然也報錯。這種嚴格的錯誤提示對咱們調試是很是有幫助的。
let a = "hey I am outside"; if(true){ //此處存在暫時性死區 console.log(a);//Uncaught ReferenceError: a is not defined let a = "hey I am inside"; }
//let與const不存在變量提高 console.log(a); // Uncaught ReferenceError: a is not defined console.log(b); // Uncaught ReferenceError: b is not defined let a = 1; //Uncaught SyntaxError: Identifier 'a' has already been declared const b = 2; //不存在變量提高,所以塊級做用域外層沒法訪問 if(true){ var bar = "bar"; let baz = "baz"; const qux = "qux"; } console.log(bar);//bar console.log(baz);//baz is not defined console.log(qux);//qux is not defined const聲明則比let聲明多了一個功能,就讓目標變量的值不能再次改變,即其餘語言的常量。
基礎類型的增長 在javascript, 咱們經過typeof與Object.prototype.toString.call能夠區分出對象的類型,過去總有7種類型:undefined, null, string, number, boolean, function, object。如今又多出兩個類型,一個是es6引進的Symbol,另外一個是es2019的BigInt。
console.log(typeof 9007199254740991n); // "bigint" console.log(typeof Symbol("aaa")); // "symbol" Symbol擁有三個特性,建立的值是獨一無二的,附加在對象是不可遍歷的,不支持隱式轉換。此外Symbol上面還有其餘靜態方法,用來爲對象擴展更多功能。 咱們先看它如何表示獨一無二的屬性值。若是沒有Symbol,咱們尋常表示常量的方法是不可靠的。
const COLOR_GREEN = 1 const COLOR_RED = 2 const LALALA = 1;
function isSafe(args) { if (args === COLOR_RED) return false if (args === COLOR_GREEN) return true throw new Error(非法的傳參: ${args}
) } console.log(isSafe(COLOR_GREEN)) //true console.log(isSafe(COLOR_RED)) //false console.log(isSafe(LALALA)) //true 若是是Symbol,則符合咱們的預期:
const COLOR_GREEN = Symbol("1")//傳參能夠是字符串,數字,布爾或不填 const COLOR_RED = Symbol("2") const LALALA = Symbol("1")
function isSafe(args) { if (args === COLOR_RED) return false if (args === COLOR_GREEN) return true throw new Error(非法的傳參: ${args}
) } console.log(isSafe(COLOR_GREEN)) //true console.log(isSafe(COLOR_RED)) //false console.log(COLOR_GREEN == LALALA) //false console.log(isSafe(LALALA)) //throw error 注意,Symbol不是一個構造器,不能new。new Symbel("222")會拋錯。 第二點,過往的對象屬性都是字符串類型,若是咱們沒有用Object.defineProperty作處理,它們都能直接用for in遍歷出來。而Symbol屬性不同,遍歷不出來,所以適用作對象的私有屬性,由於你只有知道它的名字,才能訪問到它。
var a = { b: 11, c: 22 } var d = Symbol(); a[d] = 33 for(var i in a){ console.log(i, a[i]) //只有b,c } 第三點,以往的數據類型均可以與字符串相加,變成一個字符串,或者減去一個數字,隱式轉換爲數字;而Symbol則直接拋錯。
ar d = Symbol("11") console.log(d - 1) 咱們再來看它的靜態方法: Symbol.for 這相似一個Symbol(), 可是它不表示獨一無二的值,若是用Symbor.for建立了一個symbol, 下次再用相同的參數來訪問,是返回相同的symbol。
Symbol.for("foo"); // 建立一個 symbol 並放入 symbol 註冊表中,鍵爲 "foo" Symbol.for("foo"); // 從 symbol 註冊表中讀取鍵爲"foo"的 symbol
Symbol.for("bar") === Symbol.for("bar"); // true,證實了上面說的 Symbol("bar") === Symbol("bar"); // false,Symbol() 函數每次都會返回新的一個 symbol
var sym = Symbol.for("mario"); sym.toString(); 上面例子是從火狐官方文檔拿出來的,提到註冊表這樣的東西,換言之,咱們全部由Symbol.for建立的symbol都由一個內部對象所管理。 Symbol.keyFor Symbol.keyFor()方法返回一個已註冊的 symbol 類型值的key。key就是咱們的傳參,也等於同於symbol的description屬性。
let s1 = Symbol.for("111"); console.log( Symbol.keyFor(s1) ) // "111" console.log(s1.description) // "111"
let s2 = Symbol("222"); console.log( Symbol.keyFor(s2)) // undefined console.log(s2.description) // "222"
let s3 = Symbol.for(111); console.log( Symbol.keyFor(s3) ) // "111" console.log(s3.description) // "111" 須要注意的是,Symbol.for()爲 Symbol 值登記的名字,是全局環境的,能夠在不一樣的 iframe 或 service worker 中取到同一個值。
iframe = document.createElement('iframe'); iframe.src = String(window.location); document.body.appendChild(iframe); iframe.contentWindow.Symbol.for('111') === Symbol.for('111')// true
Symbol.iterator 在es6中添加了for of循環,相對於for in循環,它是直接遍歷出值。究其緣由,是由於數組原型上添加Symbol.iterator,它就是一個內置的迭代器,而for of就是執行函數的語法。像數組,字符串,arguments, NodeList, TypeArray, Set, Map, WeakSet, WeatMap的原型都加上Symbol.iterator,所以均可以用for of循環。
console.log(Symbol.iterator in new String('sss')) // 將簡單類型包裝成對象才能使用in console.log(Symbol.iterator in [1,2,3] ) console.log(Symbol.iterator in new Set(['a','b','c','a'])) for(var i of "123"){ console.log(i) //1,2 3 } 但咱們對普通對象進行for of循環則遇到異常,須要咱們自行添加。
Object.prototype[Symbol.iterator] = function() { var keys = Object.keys(this); var index = 0; return { next: () => { var obj = { value: this[keys[index]], done: index+1 > keys.length }; index++; return obj; } }; }; var a = { name:'ruby', age:13, home:"廣東" } for (var val of a) { console.log(val); } Symbol.asyncIterator Symbol.asyncIterator與for await of循環一塊兒使用,見上面異步一節。 Symbol.replace、search、split 這幾個靜態屬性都與正則有關,咱們會發現這個方法名在字符串也有相同的臉孔,它們就是改變這些方法的行爲,讓它們能接收一個對象,這些對象有相應的symbol保護方法。具體見下面例子:
class Search1 { constructor(value) { this.value = value; } Symbol.search { return string.indexOf(this.value); } }
console.log('foobar'.search(new Search1('bar'))); class Replace1 { constructor(value) { this.value = value; } Symbol.replace { return s/${string}/${this.value}/g
; } }
console.log('foo'.replace(new Replace1('bar'))); class Split1 { constructor(value) { this.value = value; } Symbol.split { var index = string.indexOf(this.value); return this.value + string.substr(0, index) + "/" + string.substr(index + this.value.length); } }
console.log('foobar'.split(new Split1('foo'))); Symbol.toStringTag 能夠決定自定義類的 Object.prototype.toString.call的結果:
class ValidatorClass { get Symbol.toStringTag { return 'Validator'; } }
console.log(Object.prototype.toString.call(new ValidatorClass())); // expected output: "[object Validator]" 此外,還有許多靜態屬性, 方便咱們對語言的底層作更精緻的制定,這裏就不一一羅列了。 咱們再看BigInt, 它就沒有這麼複雜。早期JavaScript的整數範圍是2的53次方減一的正負數,若是超過這範圍,數值就不許確了。
console.log(1234567890123456789 * 123) //這顯然不對 所以咱們很是須要這樣的數據類型,在它沒有出來前只能使用字符串來模擬。而後chrome67中,已經內置這種類型了。想使用它,可能直接在數字後加一個n,或者使用BigInt建立它。
const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991); // ↪ 9007199254740991n
const hugeString = BigInt("9007199254740991"); // ↪ 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff"); // ↪ 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111"); console.log(typeof hugeBin) //bigint
反射機制的完善
反射機制指的是程序在運行時可以獲取自身的信息。例如一個對象可以在運行時知道本身哪些屬性被執行了什麼操做。 最早映入咱們眼簾的是IE8帶來的get, set關鍵字。這就是其餘語言的setter, getter。看似是一個屬性,實際上是兩個方法。
var inner = 0; var obj = { set a(val){ console.log("set a ") inner = val }, get a(){ console.log("get a ") return inner +2 } } console.log(obj) obj.a = 111 console.log(obj.a) // 113
image_1dojfhdi1vqbdqg1hr4mkt52h9.png-11.9kB
但在babel.js尚未誕生的年代,新語法是很難生存的,所以IE8又搞了兩個相似的API,用來定義setter, getter:Object.defineProperty與Object.defineProperties。後者是前者的強化版。
var inner = 0; var obj = {} Object.defineProperty(obj, 'a', { set:function(val){ console.log("set a ") inner = val }, get: function(){ console.log("get a ") return inner +2 } }) console.log(obj) obj.a = 111 console.log(obj.a) // 113 而標準瀏覽器怎麼辦?IE8時代,firefox一方也有相應的私有實現:defineGetter, defineSetter,它們是掛在對象的原型鏈上。
var inner = 0; var obj = {} obj.defineSetter("a", function(val){ console.log("set a ") inner = val }) obj.defineGetter("a", function(){ console.log("get a ") return inner + 4 })
console.log(obj) obj.a = 111 console.log(obj.a) // 115 在三大框架沒有崛起以前,是MVVM的狂歡時代,avalon等框架就是使用這些方法實現了MVVM中的VM。 setter與getter是IE停滯十多年瀦中添加的一個重要特性,讓JavaScript變得現代化,也更加魔幻。 但它們只能監聽對象屬性的賦值取值,若是一個對象開始沒有定義,後來添加就監聽不到;咱們刪除一個對象屬性也監聽不到;咱們對數組push進一個元素也監聽不到,對某個類進行實例化也監聽不到……總之,局b限仍是很大的。因而chrome某個版本添加了Object.observe(),支持異步監聽對象的各類舉動(如"add", "update", "delete", "reconfigure", "setPrototype", "preventExtensions"),可是其餘瀏覽器不支持,因而esma委員會又合計搞了另外一個逆天的東西Proxy。 Proxy 這個是es6大名鼎鼎的魔術代理對象,與Object.defineProperty同樣,沒法以舊有方法來模擬它。 下面是它的用法,其攔截器所表明的操做:
let p = new Proxy({}, {//攔截對象,上面有以下攔截器 get: function(target, name){ // obj.aaa }, set: function(target, name, value){ // obj.aaa = bbb }, construct: function(target, args) { //new }, apply: function(target, thisArg, args) { //執行某個方法 }, defineProperty: function (target, name, descriptor) { // Object.defineProperty() }, deleteProperty: function (target, name) { //delete }, has: function (target, name) { // in }, ownKeys: function (target, name) { // Object.getOwnPropertyNames() // Object.getOwnPropertySymbols() // Object.keys() Reflect.ownKeys() }, isExtensible: function(target) { // Object.isExtensible()。 }, preventExtensions: function(target) { // Object.preventExtensions() }, getOwnPropertyDescriptor: function(target, prop) { // Object.getOwnPropertyDescriptor() }, getPrototypeOf: function(target){ // Object.getPrototypeOf(), // Reflect.getPrototypeOf(), // proto // Object.prototype.isPrototypeOf()與instanceof }, setPrototypeOf: function(target, prototype) { // Object.setPrototypeOf(). } }); 這個對象在vue3, mobx中被大量使用。 Reflect Reflect與Proxy一同推出,Reflect上的方法與Proxy的攔截器同名,用於一些Object.xxx操做與in, new , delete等關鍵字的操做(這時只是將它們變成函數方式)。換言之,Proxy是接活的,Reflect是幹活的,火狐官網的示例也體現這一點。
var p = new Proxy({ a: 11 }, { deleteProperty: function (target, name) { console.log(arguments) return Reflect.deleteProperty(target, name) } }) delete p.a 它們與Object.xxx最大的區別是,它們都有返回結果, 而且傳參錯誤不會報錯(如Object.defineProperty)。可能官方認爲將這些元操做方法放到Object上有點不妥,因而推出了Reflect。 Reflect總共有13個靜態方法:
Reflect.apply(target, thisArg, args) Reflect.construct(target, args) Reflect.get(target, name, receiver) Reflect.set(target, name, value, receiver) Reflect.defineProperty(target, name, desc) Reflect.deleteProperty(target, name) Reflect.has(target, name) Reflect.ownKeys(target) Reflect.isExtensible(target) Reflect.preventExtensions(target) Reflect.getOwnPropertyDescriptor(target, name) Reflect.getPrototypeOf(target) Reflect.setPrototypeOf(target, prototype)
更順手的語法糖
除了添加這些方法外,JavaScript底層的parser也大動手術,讓它支持更多語法糖。語法糖均可以寫成對應的函數,但不方便。總的來講,語法糖是想讓你們的代碼更加精簡。 新近添加以下語法糖: 對象簡寫,參看類的組織形式
擴展運算符(…),用於對象的淺拷貝
箭頭函數,省略function關鍵字,與數學公式走近,能綁定this與略去return
for of(遍歷可迭代對象的全部值, for in是遍歷對象的鍵或索引)
數字格式化, 如1_222_333
字符串模板化與自然多行支持,如hello ${world}
冪運算符, **
可選鏈,let x = foo?.bar.baz();
空值合併運算符, let x = foo ?? bar();
函數的默認參數
總結
ECMAScript正在快速發展,常常會有新特性被引入,有興趣能夠查詢babel的語法插件(www.babeljs.cn/docs/plugin… 做者簡介:司徒正美,擁有十年純前端經驗,著有《JavaScript框架設計》一書,去哪兒網公共技術部前端架構師。愛好開源,擁有mass、Avalon、nanachi等前端框架。目前在主導公司的小程序、快應用的研發項目。 【END】 ———————————————— 版權聲明:本文爲CSDN博主「CSDN資訊」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接及本聲明。 原文連接:blog.csdn.net/csdnnews/ar…