前端日拱一卒D10——ES6筆記之新特性篇

DerekZ95發表於2018-08-06

前言

餘為前端菜鳥,感姿勢水平匱乏,難觀前端之大局。遂決定循前端知識之脈絡,以興趣為引,輔以幾分堅持,望於己能解惑致知、於同道能助力一二,豈不美哉。

本系列程式碼及文件均在 此處

依然很忙,繼續啃老本。。。

lesson1 Symbol

概述

  • javaScript第七種原始資料型別Symbol

    let s = Symbol('foo')

    通過Symbol函式生成,每個Symbol型別的變數值都獨一無二,作為一種類似於字串的資料結構,可以避免變數名衝突

    Symbol函式接收一個引數用於描述該Symbol例項,不影響生成的Symbol例項的值

    Symbol值不能與其他型別進行運算(模板字串中也不可以),可以顯示轉為字串和布林值(String(), Boolean())

  • Symbol作為屬性名

    • 不能用.,因為.是去取字串對應屬性名
    • 在物件中使用作為屬性名時,需使用[s]否則也會被當做字串
  • Symbol.for, Symbol.keyFor

    let s1 = Symbol.for("foo");
    Symbol.keyFor(s1) // "foo"
    let s2 = Symbol("foo"); // 先搜尋全域性,已存在該key則返回已存在的
    Symbol.keyFor(s2) // undefined
    複製程式碼

內建Symbol值

  • Symbol.hasInstance

    物件的Symbol.hasInstance屬性指向一個內部方法,其他物件使用instanceOf判斷例項時,會呼叫這個內部方法

    class Even {
      static [Symbol.hasInstance](obj) {
        return Number(obj) % 2 === 0;
      }
    }
    1 instanceOf Even
    複製程式碼
  • Symbol.isConcatSpreadable

    表示該物件用於Array.prototype.concat()時是否可以展開,陣列預設可展開,預設值為undefined,物件預設不可展開

  • Symbol.species

    指向當前物件的建構函式,創造例項時會呼叫這個方法,即使用該屬性返回的函式作為建構函式

    static get [Symbol.species]() {
      return this;
    }
    複製程式碼
  • Symbol.match, Symbol.replace, Symbol.split, Symbol.search

  • Symbol.iterator

    指向該物件的預設遍歷器方法

    物件進行for...of迴圈時,會呼叫Symbol.iterator方法,返回該物件的預設遍歷器

    詳見後續章節

  • Symbol.toPrimitive

  • Symbol.toStringTag 指向一個方法,在該物件上呼叫Object.prototype.toString()時,如果該屬性存在,則他的返回值會出現在toString方法返回的字串之中,比如[Object Array]

    新增內建物件舉個例子:JSON[Symbol.toStringTag]:'JSON'

lesson2 Set和Map

Set

基本

  • Set建構函式生成,成員值唯一,(判斷與===區別在於NaN),兩個空物件視為不同

  • 例項屬性和方法

    • 屬性: Set.prototype.constructor, Set.prototype.size
    • 方法: add(value), delete(value), has(value), clear()
    Array.from可以將Set轉為陣列
    // 陣列去重
    function dedupe(array) {
      return Array.from(new Set(array));
      // return [...new Set(array)]
    }
    複製程式碼
  • 遍歷

    • keys(), values(), entries(), forEach() 方法
    • 遍歷順序為插入順序,或可用於設定指定順序的回撥函式
    • Set的鍵名鍵值相同
    • Set預設可遍歷,預設遍歷器生成函式是values方法,這意味著,可以省略values方法,直接用for...of迴圈遍歷 Set Set.prototype[Symbol.iterator] === Set.prototype.values
    • ...內部使用for ... of,故可以使用[...Set],轉為陣列後可以方便使用陣列方法如map和filter

WeakSet

  • 成員只能為物件
  • 弱引用,垃圾回收機制對物件引用計數時不考慮WeakSet中對物件的引用
  • new WeakSet() 可以接收任何具有 Iterable 介面的物件作為引數,但必須注意加入WeakSet的成員必須為物件
  • WeakSet有以下三個方法:add(value), delete(value), has(value),沒有size屬性,不可遍歷(沒有forEach和clear方法)

Map

基本

  • 鍵值對的集合(Hash結構),鍵和物件不一樣,不侷限於字串。

  • 任何具有 Iterator 介面、且每個成員都是一個雙元素的陣列的資料結構都可以作為Map建構函式的引數

  • 只有對同一個物件的引用或者嚴格相等的簡單型別(包括NaN)才會生成一樣的Map

  • 例項屬性和方法

    • 屬性: Map.prototype.constructor, Map.prototype.size
    • 方法: set(), get(), delete(value), has(value), clear()
  • 遍歷

    • 類似上述Set的遍歷
    • Map 結構的預設遍歷器介面(Symbol.iterator屬性),就是entries方法

與其他資料結構轉換

  • 陣列,物件,JSON互轉
    function strMapToObj(strMap) {
      let obj = Object.create(null);
      for (let [k,v] of strMap) {
        obj[k] = v;
      }
      return obj;
    }
    複製程式碼

WeakMap

  • 鍵名只能為物件

  • WeakMap的鍵名所指向的物件,不計入垃圾回收機制

  • WeakMap有以下三個方法:get, set, delete(value), has(value),沒有size屬性,不可遍歷(沒有forEach和clear方法)

    前端日拱一卒D10——ES6筆記之新特性篇

lesson3 Proxy

觀察

  • 舉個栗子 當你為物件a賦值a.b=c時,你希望在b屬性賦值時有一個範圍大小的校驗,超出範圍拋錯,這個時候我們可能會想到過載set方法,比如:
    let a = {}
    Object.defineProperty(a, 'b', {
      set(x) {
        if (x>100) {
          throw new RangeError('invalid range')
        }
        this.b = x
      }
    })
    複製程式碼
    動手以後發現一個問題...這樣會棧溢位,因為在set內再set了b的值,無限迴圈...變通一下:
    let a = {}
    Object.defineProperty(a, 'b', {
      get(x) {
        return this.c
      }
      set(x) {
        if (x>100) {
          throw new RangeError('invalid range')
        }
        this.c = x
      }
    })
    複製程式碼
    然而總要這麼寫感覺很麻煩,而且如果是對一類屬性進行操作時,重複寫很沒必要,換用Proxy寫法:
    let a = {}
    let handler = {
      set(obj, prop, value, receiver) {
        if (prop === 'b') {
          if (value>100) {
            throw new RangeError('invalid range')
          }
        }
        obj[prop] = value
      }
    }
    let proxy = new Proxy(a, handler)
    複製程式碼
    看起來也舒服多了,而且可以根據屬性名在set方法內做判斷,更可擴充套件

庖丁解牛

  • 代理proxy

    let target = {};
    let handler = {};
    let proxy = new Proxy(target, handler);
    // 將代理的所有內部方法轉發至目標
    proxy.a = 1 => target.a = 1;
    target.b = 4 => proxy.b = 4;
    target !== proxy
    target.__proto__ === proxy.__proto__
    // 應在代理物件上操作,代理才能生效
    handler = {get(){return 12}}
    target.v // undefined
    proxy.v // 12
    複製程式碼
  • Proxy支援的攔截操作

    get(target, propKey, receiver) // proxy.foo, proxy['foo']
    set(target, propKey, value, receiver) //proxy.foo = v, proxy['foo'] = v
    has(target, propKey) // propKey in proxy
    deleteProperty(target, propKey) // delete proxy[propKey]
    ownKeys(target) // Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy)
    getOwnPropertyDescriptor(target, propKey) // Object.getOwnPropertyDescriptor(proxy, propKey)
    defineProperty(target, propKey, propDesc) // Object.defineProperty(proxy, propKey, propDesc), Object.defineProperties(proxy, propDescs)
    preventExtensions(target) // Object.preventExtensions(proxy)
    getPrototypeOf(target) // Object.getPrototypeOf(proxy)
    isExtensible(target) // Object.isExtensible(proxy)
    setPrototypeOf(target, proto) // Object.setPrototypeOf(proxy, proto)
    apply(target, object, args) // 攔截 Proxy 例項作為函式呼叫的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)
    construct(target, args) // new proxy(...args)
    複製程式碼
  • 代理控制程式碼handler 控制程式碼物件的方法可以複寫代理的內部方法,具體為上述的14種。

    • 舉個?

      function Tree() {
        return new Proxy({}, handler);
      }
      var handler = {
        get: function (target, key, receiver) {
          if (!(key in target)) {
            target[key] = Tree();  // 自動建立一個子樹
          }
          return Reflect.get(target, key, receiver);
        }
      }
      var tree = new Tree()
      tree.branch1.branch2.twig = "green"
      複製程式碼
    • 再來個?

      // 實現對in操作符隱藏屬性
      var handler = {
        has (target, key) {
          if (key[0] === '_') {
            return false;
          }
          return key in target;
        }
      };
      var target = { _prop: 'foo', prop: 'foo' };
      var proxy = new Proxy(target, handler);
      '_prop' in proxy // false
      複製程式碼
    • 特別注意

      如果目標物件不可擴充套件或者目標物件的屬性不可寫或者不可配置時,代理不能生效,可能會報錯

      需注意一些特定的方法對返回值有要求,不如重寫isExtensible方法時,返回值與目標物件的isExtensible屬性應一致,否則會報錯

      利用代理重寫可以做很多事情比如隱藏屬性、對某些屬性、操作符遮蔽、攔截內在方法並且加上自己想要的邏輯處理去得到預期結果等

飯後甜點

  • Proxy.revocable

    返回一個物件,proxy屬性對應Proxy例項,revoke屬性為revoke方法可以取消Proxy例項

    ```js
    let {proxy, revoke} = Proxy.revocable(target, handler);
    proxy.foo = 1
    revoke()
    proxy.foo // TypeError: Revoked
    ```
    複製程式碼
  • this問題

    • 代理以後目標物件內部的this指向的是Proxy例項而不是目標物件
    • 有時候可能因為this指向問題導致代理達不到預期效果
      // jane的name屬性實際儲存在外部的WeakMap物件的_name上,導致後續取不到值
      const _name = new WeakMap();
      class Person {
        constructor(name) {
          _name.set(this, name);
        }
        get name() {
          return _name.get(this);
        }
      }
      const jane = new Person('Jane');
      jane.name // 'Jane'
      const proxy = new Proxy(jane, {});
      proxy.name // undefined
      複製程式碼
    • 某些原生物件的部分屬性需要this指向原生物件時才能獲取,如Date.getDate(),此時proxy get時需要注意this繫結原始物件
      const target = new Date('2015-01-01');
      const handler = {
        get(target, prop) {
          if (prop === 'getDate') {
            return target.getDate.bind(target);
          }
          return Reflect.get(target, prop);
        }
      };
      const proxy = new Proxy(target, handler);
      proxy.getDate() // 1
      複製程式碼

進階

  • TO BE CONTINUED!

lesson4 Reflect

初識

  • Reflect物件與Proxy物件一樣,是為了操作物件而提供的新API,存在的原因如下:
    • 將Object物件的一些內部方法新增到Reflect物件上,且以後的新方法都部署到Reflect物件上,完成分離
    • 讓物件操作變成函式行為
    • 修改Object物件一些內部方法在出錯時的返回
    • Proxy覆寫物件方法時,提供一個Reflect物件用來獲取原始方法,以設定預設值,再此基礎上再做功能新增和修改

揭面

  • 靜態方法

    對應於Proxy可覆寫的方法,有13個靜態方法

  • 注意

    • Proxy和Reflect聯用的時候要小心,可能一個攔截會觸發另一個攔截
    let p = {
      a: 'a'
    };
    let handler = {
      set(target, key, value, receiver) {
        console.log('set');
        Reflect.set(target, key, value, receiver)
      },
      defineProperty(target, key, attribute) {
        console.log('defineProperty');
        Reflect.defineProperty(target, key, attribute);
      }
    };
    let obj = new Proxy(p, handler);
    obj.a = 'A';
    // set
    // defineProperty
    複製程式碼
      在Reflect.set傳入receiver的時候觸發了Proxy.defineProperty,不傳入receiver時不會觸發defineProperty攔截
    複製程式碼
    • 對於引數的要求、轉換和報錯處理

?

  • 使用Proxy實現觀察者模式
    const person = observable({
      name: '張三',
      age: 20
    });
    function print() {
      console.log(`${person.name}, ${person.age}`)
    }
    observe(print);
    person.name = '李四';
    // 輸出
    // 李四, 20
    /**************************/
    const queuedObservers = new Set();
    const observe = fn => queuedObservers.add(fn);
    const observable = obj => new Proxy(obj, {set});
    function set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      queuedObservers.forEach(observer => observer());
      return result;
    }
    複製程式碼

lesson5 遍歷器Iterator

遇見

  • why Iterator

    js中資料集合的概念越來越多,如果能有一種統一的訪問方式將是極好的。Iterator的設計就基於此,通過為相應資料結構部署iterator介面讓該資料結構可通過統一的方式:for...of遍歷

  • 遍歷過程:

    • 建立一個指標物件指向當前資料結構的初始位置(遍歷器物件實際為一個指標物件)
    • 呼叫指標物件的next方法,直到指向資料結構的結束位置
    // 遍歷器生成函式
    function makeIterator(array) {
      var nextIndex = 0;
      return {
        next: function() {
          return nextIndex < array.length ?
            {value: array[nextIndex++]} :
            {done: true};
        }
      };
    }
    複製程式碼
  • 一種資料結構,只要部署了Iterator介面,就視為可遍歷的

相識

預設Iterator介面

  • 預設的Iterator介面部署在[Symbol.iterator]屬性上,Symbol.iterator屬性鍵為Symbol物件,值為一個函式,即遍歷器生成函式,執行該函式會返回一個遍歷器物件,該物件具有一個next方法,呼叫該方法可以返回{value, done}物件,代表了當前成員的資訊
  • 部分資料結構如Array、Set、Map、String等已經部署了Iterator介面,物件則需要手動新增這樣的方法實現Iterator介面
  • 對於非線性資料結構,Iterator介面實際上就是一種線性轉換,下例為class實現遍歷器
    class RangeIterator {
      constructor(start, stop) {
        this.value = start;
        this.stop = stop;
      }
      [Symbol.iterator]() { return this; }
      next() {
        var value = this.value;
        if (value < this.stop) {
          this.value++;
          return {done: false, value: value};
        }
        return {done: true, value: undefined};
      }
    }
    function range(start, stop) {
      return new RangeIterator(start, stop);
    }
    for (var value of range(0, 3)) {
      console.log(value); // 0, 1, 2
    }
    複製程式碼

舉個?

  • 實現指標

    function Node(value) {
      this.value = value;
      this.next = null
    }
    // for...of時會呼叫改遍歷器生成函式
    Node.prototype[Symbol.iterator]= function() {
      // 返回的遍歷器物件
      var iterator = {
        next: next
      }
      // 當前成員
      var current = this
      next() {
        if(current) {
          var value = current.value;
          // 移動指標
          current = current.next;
          return { done: false, value: value };
        }
        return { done: true, value: undefined };
      }
      return iterator
    }
    // 新建物件,因為在原型上實現的遍歷器生成函式,所以每個例項都實現了遍歷器介面
    var one = new Node(1);
    var two = new Node(2);
    var three = new Node(3);
    // 當前成員的next指向下一個成員,在next方法中實現指標移動
    one.next = two;
    two.next = three;
    // 對物件使用for...of時,去查詢[Symbol.iterator]屬性,找到後迴圈呼叫next方法,直到返回值得done屬性為true
    for (var i of one){
      console.log(i); // 1, 2, 3
    }
    複製程式碼
  • 如果Symbol.iterator方法對應的不是遍歷器生成函式,則會報錯

在何方

呼叫場合

  • 解構賦值

  • 擴充套件運算子

    任何實現了Iterator介面(可遍歷)的資料結構都可以通過...將其轉化為陣列

  • yield*後跟一個可遍歷資料結構時,會呼叫該結構的遍歷器介面

  • for...of, Array.from, Map, Set, Promise.all(), Promise.race()

字串、陣列等的遍歷器

  • 字串

    for...of能夠正確識別32位UTF-16字元

    var someString = "hi";
    typeof someString[Symbol.iterator]
    // "function"
    var iterator = someString[Symbol.iterator]();
    // 當然你也可以修改Symbol.iterator方法達到你想要的遍歷結果
    iterator.next()  // { value: "h", done: false }
    iterator.next()  // { value: "i", done: false }
    iterator.next()  // { value: undefined, done: true }
    複製程式碼
  • return, throw方法

    這兩個方法都是在設定遍歷器生成函式時可選的,一般配合generator使用,所以下次再說

  • 陣列

    • for ... of 只返回具有數字索引的鍵值的值
    • 類陣列物件可以使用陣列的預設遍歷生成器達到遍歷效果(要求是數字索引以及具有length屬性)
  • Map, Set

    // Map遍歷返回的是陣列[k, v],Set返回的是值
    for (let [key, value] of map) {
      console.log(key + ' : ' + value);
    }
    複製程式碼
  • 類陣列物件

    利用Array.from將其轉化為陣列,再使用陣列的遍歷器介面用for...of實現遍歷

for ... of 注意

  • 可以結合break、continue、return使用
  • 提供了多種資料結構的統一訪問方式
  • 相比for ... in,後者遍歷的是鍵,且鍵名為字串,還可能會遍歷原型上的鍵

雖發表於此,卻畢竟為一人之言,又是每日學有所得之筆記,內容未必詳實,看官老爺們還望海涵。

相關文章