JavaScript 為什麼能活到現在?

CSDN資訊發表於2020-01-25
 
640?wx_fmt=gif
640?wx_fmt=jpeg
作者 | 司徒正美
責編 | 郭芮
出品 | 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,讓它變成一個正統的,魔幻的語言。
  • 原型方法的極大豐富;

  • 類與模組的標準化;

  • 非同步機制的嬗變;

  • 塊級作用域的補完;

  • 基礎型別的增加;

  • 反射機制的完善;

  • 更順手的語法糖。

 
 
 
 
 
 
 
 
640?wx_fmt=png
原型方法的極大豐富
 
原型方法自Prototype.js出來後,就不斷被招安成官方API。基本上在字串與陣列這兩大類別擴充,它們在日常業務中不斷被使用,因此不斷變重複造輪子,因此亟待官方化。

640?wx_fmt=png

JavaScript的版本說明:

640?wx_fmt=png

這些原型方法非常有用,以致於在面試中經常被問到,如果去除字串兩邊的空白,如何扁平化一個陣列?
 
640?wx_fmt=png
類與模組的標準化
 
在沒有類的時代,每個流行框架都會帶一個建立類的方法,可見大家都不太認同原型這種複用機制。
下面是原型與類的寫法比較:

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

 
<script src="zepto.js"></script>
<script src="jhash.js"></script>
<script src="fastClick.js"></script>
<script src="iScroll.js"></script>
<script src="underscore.js"></script>
<script src="handlebar.js"></script>
<script src="datacenter.js"></script>
<script src="util/wxbridge.js"></script>
<script src="util/login.js"></script>
<script src="util/base.js"></script>
於是後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

 
640?wx_fmt=png
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 rel="modulepreload" href="lib.mjs">
<link rel="modulepreload" href="main.mjs">
<script type="module" src="main.mjs"></script>
<script nomodule src="fallback.js"></script>
但可惜的是,瀏覽器對模組系統的支援是非常滯後,並且即便最新的瀏覽器支援了,我們還是免不了要相容舊的瀏覽器。對此,我們只能奠出webpack這利器,它是前端工程化的集大成者,可以將我們的程式碼通過各種loader/plugin打包成主流瀏覽器都認識的JavaScript語法,並以最原始的方式掛載進去。
 
640?wx_fmt=png
非同步機制的嬗變
 
在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: '<b>bold</b>',
  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}
640?wx_fmt=png
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<int> 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)
640?wx_fmt=png
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();
  }
}

 

640?wx_fmt=png

塊級作用域的補完

 

說起作用域,大家一般認為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. 作用域是區域性,作用範圍是括起它的兩個花括號間,即for(){}, while(){}, if(){}與單純的{}
  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宣告多了一個功能,就讓目標變數的值不能再次改變,即其他語言的常量。
 
640?wx_fmt=png
基礎型別的增加
 
在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](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

 

640?wx_fmt=png

反射機制的完善

 

反射機制指的是程式在執行時能夠獲取自身的資訊。例如一個物件能夠在執行時知道自己哪些屬性被執行了什麼操作。
最先映入我們眼簾的是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
640?wx_fmt=png
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)

 

640?wx_fmt=png
更順手的語法糖

 

除了新增這些方法外,JavaScript底層的parser也大動手術,讓它支援更多語法糖。語法糖都可以寫成對應的函式,但不方便。總的來說,語法糖是想讓大家的程式碼更加精簡。
新近新增如下語法糖:
  • 物件簡寫,參看類的組織形式

  • 擴充套件運算子(),用於物件的淺拷貝

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

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

  • 數字格式化, 如1_222_333

  • 字串模板化與天然多行支援,如hello ${world}

  • 冪運算子, **

  • 可選鏈,let x = foo?.bar.baz();

  • 空值合併運算子, let x = foo ?? bar();

  • 函式的預設引數

 

640?wx_fmt=png
總結

 

ECMAScript正在快速發展,經常會有新特性被引入,有興趣可以查詢babel的語法外掛(https://www.babeljs.cn/docs/plugins),瞭解更詳細的用法。相信有了這些新特徵的支援,大家再也不敢看小JavaScript了。
作者簡介:司徒正美,擁有十年純前端經驗,著有《JavaScript框架設計》一書,去哪兒網公共技術部前端架構師。愛好開源,擁有mass、Avalon、nanachi等前端框架。目前在主導公司的小程式、快應用的研發專案。

【END】

0基礎學習python,看這十個案例,讓你很快上手python!

https://edu.csdn.net/topic/python115?utm_source=csdn_bw

640?wx_fmt=jpeg

 熱 文 推 薦 

 

相關文章