JavaScript 為什麼能活到現在?

JavaScript 為什麼能活到現在?

作者 | 司徒正美

出品 | 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 为什么能活到现在?

原型方法的極大豐富

原型方法自Prototype.js出來後,就不斷被招安成官方API。基本上在字符串與數組這兩大類別擴充,它們在日常業務中不斷被使用,因此不斷變重複造輪子,因此企待官方化。

JavaScript 为什么能活到现在?

JavaScript的版本說明:

JavaScript 为什么能活到现在?

這些原型方法非常有用,以致於在面試中經常被問到,如果去除字符串兩邊的空白,如何扁平化一個數組?

JavaScript 为什么能活到现在?

類與模塊的標準化

在沒有類的時代,每個流行框架都會帶一個創建類的方法,可見大家都不太認同原型這種複用機制。

下面是原型與類的寫法比較:

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文件沒有統一的編寫規範,意味著想把它們打包一個是非常困難的,只能像下面那樣平鋪著。這些文件的依賴關係,只有最初的人知道,要了幾輪開發後,就是定時炸彈。此外,不要忘記,<code><script>/<code>標準還會導致頁面渲染堵塞,出現白屏現象。

於是後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 $ = require("jquery");

$("#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 美刀賞金活動,讓國內高手一展身手。

  • https://github.com/seajs/seajs/issues/478

JavaScript 为什么能活到现在?

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標籤添加新的屬性或屬性值來支持這新特性。(詳見:https://

www.jianshu.com/p/f7db50cf956f)

<link>

<link>

但可惜的是,瀏覽器對模塊系統的支持是非常滯後,並且即便最新的瀏覽器支持了,我們還是免不了要兼容舊的瀏覽器。對此,我們只能奠出webpack這利器,它是前端工程化的集大成者,可以將我們的代碼通過各種loader/plugin打包成主流瀏覽器都認識的JavaScript語法,並以最原始的方式掛載進去。

JavaScript 为什么能活到现在?

異步機制的嬗變

在JavaScript沒有大規模應用前,用到異步的地方只有ajax請求與動畫,在請求結束與動畫結束時要做什麼事,使用的辦法是經典的回調。

回調

由於javascript是單線程的,我們的方法是同步的,像下面這樣,一個個執行:

A;

B;

C;

而異步則是不可預測其觸發時機:

A;

// 在現在發送請求

ajax({

url: url,

data: {},

success:function(res){

// 在未來某個時刻執行

B(res)

}

})

C;

//執行順序:A -> C -> B

回調函數是主函數的後繼方法,基本上能保證,主函數執行後,它能在之後某個時刻被執行一次。但隨著功能的細分,在微信小程序或快應用中,它們拆分成三個,即一個方法跟著三個回調。

// https://doc.quickapp.cn/features/system/share.html

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("總會執行")

})

為什麼這麼說呢?看上面的示例,<code>new Promise(executor)/<code>裡的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}

JavaScript 为什么能活到现在?

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)

JavaScript 为什么能活到现在?

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 为什么能活到现在?

塊級作用域的補完

說起作用域,大家一般認為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,有如下特點:

  1. 作用域是局部,作用範圍是括起它的兩個花括號間,即<code>for{}/<code>,<code>while{}/<code>,<code>if{}/<code>與單純的<code>{}/<code>。

  2. 它也不會提升到作用域頂部,它頂部到定義的那一行變稱之為“暫時性死區”,這時使用它會報錯。

  3. 變量一旦變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 为什么能活到现在?

基礎類型的增加

在javascript, 我們通過typeof與Object.prototype.toString.call可以區分出對象的類型,過去總有7種類型:undefined, , string, number, boolean, function, object。現在又多出兩個類型,一個是es6引進的Symbol,另一個是es2019的bBigInt。

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。<code>new Symbel("222")/<code>會拋錯。

第二點,過往的對象屬性都是字符串類型,如果我們沒有用Object.defineProperty做處理,它們都能直接用<code>for in/<code>遍歷出來。而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中添加了<code>for of/<code>循環,相對於<code>for in/<code>循環,它是直接遍歷出值。究其原因,是因為數組原型上添加Symbol.iterator,它就是一個內置的迭代器,而<code>

for of/<code>就是執行函數的語法。像數組,字符串,arguments, NodeList, TypeArray, Set, Map, WeakSet, WeatMap的原型都加上Symbol.iterator,因此都可以用<code>for of/<code>循環。

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

}

但我們對普通對象進行<code>for of/<code>循環則遇到異常,需要我們自行添加。

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與<code>for await of/<code>循環一起使用,見上面異步一節。

Symbol.replace、search、split

這幾個靜態屬性都與正則有關,我們會發現這個方法名在字符串也有相同的臉孔,它們就是改變這些方法的行為,讓它們能接收一個對象,這些對象有相應的symbol保護方法。具體見下面例子:

class Search1 {

constructor(value) {

this.value = value;

}

[Symbol.search](string) {

return string.indexOf(this.value);

}

}

console.log('foobar'.search(new Search1('bar')));

class Replace1 {

constructor(value) {

this.value = value;

}

[Symbol.replace](string) {

return `s/${string}/${this.value}/g`;

}

}

console.log('foo'.replace(new Replace1('bar')));

class Split1 {

constructor(value) {

this.value = value;

}

[Symbol.split](string) {

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

JavaScript 为什么能活到现在?

反射機制的完善

反射機制指的是程序在運行時能夠獲取自身的信息。例如一個對象能夠在運行時知道自己哪些屬性被執行了什麼操作。

最先映入我們眼簾的是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

JavaScript 为什么能活到现在?

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一方也有相應的私有實現:<code>__defineGetter__/<code>,<code>__defineSetter__/<code>,它們是掛在對象的原型鏈上。

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,支持異步監聽對象的各種舉動(如"a

dd", "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 为什么能活到现在?

更順手的語法糖

除了添加這些方法外,JavaScript底層的parser也大動手術,讓它支持更多語法糖。語法糖都可以寫成對應的函數,但不方便。總的來說,語法糖是想讓大家的代碼更加精簡。

新近添加如下語法糖:

  • 對象簡寫,參看類的組織形式

  • 擴展運算符(<code>/<code>),用於對象的淺拷貝

  • 箭頭函數,省略function關鍵字,與數學公式走近,能綁定this與略去return

  • for of(遍歷可迭代對象的所有值, for in是遍歷對象的鍵或索引)

  • 數字格式化, 如1_222_333

  • 字符串模板化與天然多行支持,如<code>

    hello ${world}/<code>

  • 冪運算符, <code>**/<code>

  • 可選鏈,<code>let x = foo?.bar.baz;/<code>

  • 空值合併運算符, <code>let x = foo ?? bar;/<code>

  • 函數的默認參數

JavaScript 为什么能活到现在?

總結

ECMAScript正在快速發展,經常會有新特性被引入,有興趣可以查詢babel的語法插件(https://www.babeljs.cn/docs/plugins),瞭解更詳細的用法。相信有了這些新特徵的支持,大家再也不敢看小JavaScript了。

作者簡介:司徒正美,擁有十年純前端經驗,著有《JavaScript框架設計》一書,去哪兒網公共技術部前端架構師。愛好開源,擁有mass、Avalon、nanachi等前端框架。目前在主導公司的小程序、快應用的研發項目。

【END】


分享到:


相關文章: