你不可不知的ES6

本文為阮一峰大神的《ECMAScript 6 入門》的個人版提純!

babel

babel負責將JS高級語法轉義,使之能被各種瀏覽器所執行。其使用步驟如下:

  1. 編寫配置文件.babelrc
    此文件存于根目錄,基本格式如下:
{
  "presets": [],
  "plugins": []
}
  1. 選定轉碼規則
    presets字段設定轉碼規則,官方提供以下的規則集:
# 最新轉碼規則
$ npm install --save-dev babel-preset-latest

# react 轉碼規則
$ npm install --save-dev babel-preset-react

# 不同階段語法提案的轉碼規則(共有4個階段),選裝一個
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3
  1. 將這些規則加入.babelrc
{
    "presets": [
      "latest",
      "react",
      "stage-2"
    ],
    "plugins": []
  }

命令行轉碼babel-cli

其使用方法如下:

// 安裝
$ npm install --save-dev babel-cli
//改寫package.json
{
  // ...
  "devDependencies": {
    "babel-cli": "^6.0.0"
  },
  "scripts": {
    "build": "babel src -d lib"
  },
}
//轉碼命令
$ npm run build

babel-register

babel-register模塊改寫require命令,為它加上一個鉤子。此后,每當使用require加載.js.jsx.es.es6后綴名的文件,就會先用 Babel 進行轉碼。由于它是實時轉碼,所以只適合在開發環境使用。

//自動對index.js轉碼
require("babel-register")
require("./index.js")
  • 如果某些代碼需要調用 Babel 的 API 進行轉碼,就要使用babel-core模塊。
  • Babel 默認只轉換新的 JavaScript 句法,而不轉換新的 API。舉例來說,ES6 在Array對象上新增了Array.from方法。Babel 就不會轉碼這個方法。如果想讓這個方法運行,必須使用babel-polyfill

let和const

  • let:創造了塊級作用域,每個“塊”中,變量不允許重名,不同的“塊”中,同名變量不會相互污染;先聲明后調用。
  • *do表達式使塊級作用域產生返回值
let x = do {
  let t = f()
  t * t + 1
}
  • const:保存的地址(指針)不得修改,值可以修改;
var a = 1
window.a  // 1
let b = 1
window.b // undefined

解構復制

  • 數組、對象、字符串的解構復制
let [a, b, c] = [1, 2, 3];
let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length : len} = 'hello';
len // 5
  • 如果=右邊不是可遍歷解構(數組等),即不具有Iterator接口,則不可解構復制!
  • 提取JSON數據
let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
}
let { id, status, data: number } = jsonData;
  • 函數參數的默認值
jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
}) {
  // ... do stuff
}//避免了let a = this.a || ’a‘這種寫法
  • 通過for...of遍歷Map
for (let [key, value] of map) {}
for (let [key] of map) {}//獲取鍵名
for (let [value] of map) {}//獲取鍵值

字符串的擴展

  • ES6 為字符串添加了遍歷器接口,使得字符串可以被for...of循環遍歷。
for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"
  • includes(), startsWith(), endsWith()
let s = 'Hello world!'
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
  • repeat()padStart()padEnd()
  • 模板
$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`)//反引號(`)和$標識

函數的擴展

  • 新增函數參數的默認值,且為單獨作用域。一旦設置了參數的默認值,函數進行聲明初始化時,參數會形成一個單獨的作用域。等到初始化結束,這個作用域就會消失。這種語法行為,在不設置參數默認值時,是不會出現的。
let x = 1;
function f(y = x) {
  let x = 2;
  console.log(y);
}
f() // 1

上面代碼中,函數f調用時,參數y = x形成一個單獨的作用域。這個作用域里面,變量x本身沒有定義,所以指向外層的全局變量x。函數調用時,函數體內部的局部變量x影響不到默認值變量x
如果此時,全局變量x不存在,就會報錯。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1
var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;//指向第一個變量x
  y();
  console.log(x);
}
foo() // 2
x // 1

上面代碼中,函數foo的參數形成一個單獨作用域。這個作用域里面,首先聲明了變量x,然后聲明了變量yy的默認值是一個匿名函數。這個匿名函數內部的變量x,指向同一個作用域的第一個參數x。函數foo內部又聲明了一個內部變量x,該變量與第一個參數x由于不是同一個作用域,所以不是同一個變量,因此執行y后,內部變量x和外部全局變量x的值都沒變。如果將var x = 3var去除,函數foo的內部變量x就指向第一個參數x,與匿名函數內部的x是一致的,所以最后輸出的就是2,而外層的全局變量x依然不受影響

  • rest參數(形式為...變量名),用于獲取不確定個數的全部參數,舍棄arguments對象(arguments為類數組的對象,不是真正的數組)
function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}
add(2, 5, 3) // 10
  • =>
    箭頭函數可以讓this指向固定。函數體內的this對象,就是定義時所在的對象(與父作用域共享this上下文,使用時即尋找父作用域this),而不是運行時所在的對象。this對象的指向是可變的,但是在箭頭函數中,它是固定的。
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭頭函數
  setInterval(() => this.s1++, 1000);
  // 普通函數
  setInterval(function () {
    this.s2++;
  }, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

上面代碼中,箭頭函數的this綁定定義時所在的作用域(即Timer函數),后面的普通函數的this指向運行時所在的作用域(即全局對象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都沒更新。
this指向的固定化,并不是因為箭頭函數內部有綁定this的機制,實際原因是箭頭函數根本沒有自己的this,導致內部的this就是外層代碼塊的this。正是因為它沒有this,所以也就不能用作構造函數;也就不能用call()apply()bind()這些方法去改變this的指向。

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}
// ES5
function foo() {
  var _this = this;
  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

由于大括號被解釋為代碼塊,所以如果箭頭函數直接返回一個對象,必須在對象外面加上括號,否則會報錯。

// 報錯
let getTempItem = id => { id: id, name: "Temp" };
// 不報錯
let getTempItem = id => ({ id: id, name: "Temp" });
  • ::,用于函數綁定,該運算符會自動將左邊的對象,作為上下文環境(即this對象),綁定到右邊的函數上面。其結果是對象,可以鏈式綁定。
  • 尾調用,指某個函數的最后一步是調用另一個函數。
function f(x){
  return g(x);
}
  • 尾調用優化,只有不再用到外層函數的內部變量,內層函數的調用幀才會取代外層函數的調用幀。

數組的擴展

  • ...
    將一個數組轉為用逗號分隔的參數序列,即用于展開數組。
    擴展運算符內部調用的是Iterator接口,因此只要具有 Iterator 接口的對象,都可以使用擴展運算符(Map,Generator等)
const arr = [
  ...(x > 0 ? ['a'] : []),//如果擴展運算符后面是一個空數組,則不產生任何效果。
  'b',
]
  • 復制數組
    數組是復合的數據類型,直接復制的話,只是復制了指向底層數據結構的指針,而不是克隆一個全新的數組。
const a1 = [1, 2];
// 寫法一
const a2 = [...a1]
// 寫法二
const [...a2] = a1
  • 合并數組
[1, 2, ...more]
var arr1 = ['a', 'b']
var arr2 = ['c']
var arr3 = ['d', 'e']
[...arr1, ...arr2, ...arr3]
  • Array.from()將類似數組的對象(任何有length屬性的對象,如DOM操作返回的NodeList集合,以及函數內部的arguments對象)和可遍歷的對象轉化為Array
// NodeList對象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
  console.log(p);
})

// arguments對象
function foo() {
  var args = Array.from(arguments);
}
  • Array.of()將一組值,轉換為數組
  • copyWithin()
    在當前數組內部,將指定位置的成員復制到其他位置(會覆蓋原有成員),然后返回當前數組。也就是說,使用這個方法,會修改當前數組。
  • find()
    用于找出第一個符合條件的數組成員。它的參數是一個回調函數,所有數組成員依次執行該回調函數,直到找出第一個返回值為true的成員,然后返回該成員。如果沒有符合條件的成員,則返回undefined
  • findIndex()
    返回第一個符合條件的數組成員的位置,如果所有成員都不符合條件,則返回-1
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
  • fill()用于初始化數組
  • entries() keys() values() 用于遍歷數組
for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"
  • includes()取代indexOf()

對象的擴展

  • 快速寫法
const foo = 'bar'
const baz = {foo}
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo}
  • 函數的name屬性,返回函數名
  • Object.is(),用于比較兩個值是否相等
  • Object.assign(target, source1, source2)將源對象(source)的所有可枚舉屬性,復制到目標對象(target),后面的覆蓋前面的同名屬性
  • Object.assign()淺拷貝,如果源對象某個屬性的值是對象,那么目標對象拷貝得到的是這個對象的引用。
const obj1 = {a: {b: 1}}
const obj2 = Object.assign({}, obj1)
obj1.a.b = 2
obj2.a.b // 2
  • Object.assign()對于數組的處理,是將其看成對象
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
  • Object.assign()為對象添加屬性和方法
class Point {
  constructor(x, y) {
    Object.assign(this, {x, y})
  }
}
Object.assign(SomeClass.prototype, {someMethod(arg1, arg2) {},
  anotherMethod() {}
})
  • Object.assign({}, origin)克隆對象,合并對象
  • 盡量不要用for...in循環,而用Object.keys()代替,為了規避掉繼承的屬性
  • 屬性的遍歷
  1. for...in,循環遍歷對象自身的和繼承的可枚舉屬性(不含 Symbol屬性)。
  2. Object.keys(obj),返回一個數組,包括對象自身的(不含繼承的)所有可枚舉屬性(不含 Symbol屬性)的鍵名。
  3. Object.getOwnPropertyNames(obj),返回一個數組,包含對象自身的所有屬性(不含 Symbol 屬性,但是包括不可枚舉屬性)的鍵名。
  4. Object.getOwnPropertySymbols(obj),返回一個數組,包含對象自身的所有 Symbol 屬性的鍵名。
  5. Reflect.ownKeys(obj),返回一個數組,包含對象自身的所有鍵名,不管鍵名是 Symbol 或字符串,也不管是否可枚舉。
//首先遍歷所有數值鍵,按照數值升序排列。
//其次遍歷所有字符串鍵,按照加入時間升序排列。
//最后遍歷所有 Symbol 鍵,按照加入時間升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
  • Object.setPrototypeOf()用于設置一個對象的prototype對象,返回參數對象本身
  • Object.setPrototypeOf()用于讀取一個對象的原型對象
  • super關鍵字,指向當前對象的原型對象
const proto = {
  foo: 'hello'
}
const obj = {
  find() {
    return super.foo;
  }
}
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
  • Object.keys()Object.values()Object.entries()
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 }
for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
  • 對象的解構賦值,用...,用于從一個對象取值,相當于將所有可遍歷的、但尚未被讀取的屬性,分配到指定的對象上面。所有的鍵和它們的值,都會拷貝到新對象上面,也可用于生成新對象,但是拷貝的是這個值的引用,不是新副本,與Object.assign()相同
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 },等同于Object.assign()
  • 深拷貝
  1. JSON.parse(JSON.stringify(initalObj))
  2. 遞歸
function deepClone(initalObj) {
    var obj = {};
    for (var i in initalObj) {
        var prop = initalObj[i];
        // 避免相互引用對象導致死循環,如initalObj.a = initalObj的情況
        if(prop === obj) {
            continue;
        } 
        if (typeof prop === 'object') {
            obj[i] = (prop.constructor === Array) ? [] : {};
            arguments.callee(prop, obj[i]);
        } else {
            obj[i] = prop;
        }
    }
    return obj;
}

Symbol

  • 一種防止屬性名沖突的機制,其表示獨一無二的值,是一種類似于字符串的數據類型
  • Symbol值可以顯式轉為字符串
  • 每一個 Symbol 值都是不相等的,這意味著 Symbol 值可以作為標識符,用于對象的屬性名,就能保證不會出現同名的屬性。這對于一個對象由多個模塊構成的情況非常有用,能防止某一個鍵被不小心改寫或覆蓋
  • Symbol 值作為對象屬性名時,不能用點運算符(點運算符后面總是字符串)
  • Object.getOwnPropertySymbols,返回一個數組,成員是當前對象的所有用作屬性名的 Symbol 值。
  • 使用時需要放在[ ]
et s = Symbol();
let obj = {
  [s]: function (arg) { ... }
};
obj[s](123)
  • Symbol 作為名稱的屬性,不會被常規方法遍歷得到。我們可以利用這個特性,為對象定義一些非私有的、但又希望只用于內部的方法。
  • Reflect.ownKeys,可以返回所有類型的鍵名,包括常規鍵名和 Symbol 鍵名。
  • Symbol.for(),接受一個字符串作為參數,然后搜索有沒有以該參數作為名稱的 Symbol 值。如果有,就返回這個 Symbol 值,否則就新建并返回一個以該字符串為名稱的 Symbol 值。
  • Symbol.keyFor(),返回一個已登記的 Symbol類型值的key
  • Symbol的11 個內置函數
    1 Symbol.hasInstance
    2 Symbol.isConcatSpreadable
    3 Symbol.species
    4 Symbol.match
    5 Symbol.replace
    6 Symbol.search
    7 Symbol.split
    8 Symbol.iterator
    9 Symbol.toPrimitive
    10 Symbol.toStringTag
    11 Symbol.unscopables

Proxy

  • Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫
  • var proxy = new Proxy(target, handler)target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定制攔截行為
  • 要使得Proxy起作用,必須針對Proxy實例進行操作,而不是針對目標對象進行操作
var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
  • Proxy 實例也可以作為其他對象的原型對象
var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});
let obj = Object.create(proxy);
obj.time // 35
  • 13種攔截操作

Reflect

  • Object對象的一些明顯屬于語言內部的方法(比如Object.defineProperty),放到Reflect對象上。現階段,某些方法同時在ObjectReflect對象上部署,未來的新方法將只部署在Reflect對象上。也就是說,從Reflect對象上可以拿到語言內部的方法
  • 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false
  • Object操作都變成函數行為。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)讓它們變成了函數行為
  • Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象可以方便地調用對應的Reflect方法,完成默認行為,作為修改行為的基礎。也就是說,不管Proxy怎么修改默認行為,你總可以在Reflect上獲取默認行為
  • 13個靜態方法一一對應proxy的13種攔截行為

Set 和 Map

  • Set類似于數組,成員無重復,即可以用來數組去重
// 去除數組的重復成員
[...new Set(array)]
  • add(value):添加某個值,返回 Set 結構本身
  • delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功
  • has(value):返回一個布爾值,表示該值是否為Set的成員
  • clear():清除所有成員,沒有返回值
  • Array.from方法可以將 Set 結構轉為數組
  • Set的遍歷操作
    1 keys():返回鍵名的遍歷器,keys方法和values方法的行為完全一致
    2 values():返回鍵值的遍歷器,keys方法和values方法的行為完全一致
    3 entries():返回鍵值對的遍歷器
    4 forEach():使用回調函數遍歷每個成員
  • WeakSetSet類似,成員不重復,但只能是對象。WeakSet 適合臨時存放一組對象,以及存放跟對象綁定的信息。只要這些對象在外部消失,它在 WeakSet 里面的引用就會自動消失;WeakSet 的成員是不適合引用的,因為它會隨時消失。另外,由于 WeakSet 內部有多少個成員,取決于垃圾回收機制有沒有運行,運行前后很可能成員個數是不一樣的,而垃圾回收機制何時運行是不可預測的,因此 ES6 規定 WeakSet 不可遍歷。
  • Map,類似于對象,也是鍵值對的集合,但是“鍵”的范圍不限于字符串,各種類型的值(包括對象)都可以當作鍵。
  • Object 結構提供了“字符串—值”的對應,Map 結構提供了“值—值”的對應,是一種更完善的 Hash 結構實現。如果你需要“鍵值對”的數據結構,Map 比 Object 更合適
  • 作為構造函數,Map 也可以接受一個數組作為參數。該數組的成員是一個個表示鍵值對的數組。
const map = new Map([
  ['name', '張三'],
  ['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "張三"
map.has('title') // true
map.get('title') // "Author"
  • 任何具有 Iterator 接口、且每個成員都是一個雙元素的數組的數據結構(詳見《Iterator》一章)都可以當作Map構造函數的參數
    1 size 屬性
    2 set(key, value)
    3 get(key)
    4 has(key),返回一個布爾值
    5 delete(key),返回true
    6 clear(),清除所有對象,無返回值
  • 遍歷方法,Map 的遍歷順序就是插入順序
    1 keys():返回鍵名的遍歷器。
    2 values():返回鍵值的遍歷器。
    3 entries():返回所有成員的遍歷器。
    4 forEach():遍歷 Map 的所有成員。
  • Map 結構轉為數組結構,比較快速的方法是使用擴展運算符...
  • WeakMap 的用途,我們將這個狀態作為鍵值放在 WeakMap 里,對應的鍵名就是myElement。一旦這個 DOM 節點刪除,該狀態就會自動消失,不存在內存泄漏風險
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
  let logoData = myWeakmap.get(myElement);
  logoData.timesClicked++;
}, false);

Iterator

  • 為各種數據結構提供一個統一的訪問的接口
  • 數據結構成員按某種次序排列
  • ES6創造了for...of,Iterator供其消費,這種數據結構為可遍歷的(Symbol.iterator屬性,其本身是個遍歷器生成函數,執行后返回遍歷器)
  • 其本質是指針,每次調用返回{value:*****;done: *****}
    原生具備Iterator:
    1 Array
    2 Map
    3 Set
    4 String
    5 TypedArray
    6 函數的arguments對象
    7 NodeList對象

Promise

  • 一個保存異步結果的容器
  • 兩種狀態:pedding => resolvedpedding => rejected,發生這兩種情況時狀態立刻固化,回調函數立刻執行,與事件監聽不同
  • 如果某些事件不斷地反復發生,一般來說,使用 Stream 模式是比部署Promise更好的選擇。
  • Promise實例
const promise = new Promise((resolve, reject) => {
  if (/* 異步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
})
  • Promise新建后立刻執行,then回調本輪循環最后執行,晚于本輪事件循環的同步任務
  • 下面代碼中,p1是一個Promise,3秒之后變為rejected。p2的狀態在1秒之后改變,resolve方法返回的是p1。由于p2返回的是另一個 Promise,導致p2自己的狀態無效了,由p1的狀態決定p2的狀態。所以,后面的then語句都變成針對后者(p1)。又過了2秒,p1變為rejected,導致觸發catch方法指定的回調函數
const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})
p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail
  • then()方法,返回的是一個新的Promise實例,可以手工指定(return)需要返回的新的Promise實例,若不指定,則默認返回上一個Promise實例
  • catch()方法,Promise 對象的錯誤具有“冒泡”性質,會一直向后傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲,并且在下一輪循環中拋出
  • Promise.all()方法
  • Promise.race()方法
  • Promise.resolve()方法,將現有對象轉為Promise對象
    1 參數是一個Promise實例,那么Promise.resolve將不做任何修改、原封不動地返回這個實例
    2 參數是一個thenable對象,Promise.resolve方法會將這個對象轉為Promise對象,然后就立即執行thenable對象的then方法
    3 參數不是具有then方法的對象,或根本就不是對象
    如果參數是一個原始值,或者是一個不具有then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態為resolved。
    4 不帶有任何參數
    Promise.resolve方法允許調用時不帶參數,直接返回一個resolved狀態的Promise對象。所以,如果希望得到一個Promise對象,比較方便的方法就是直接調用Promise.resolve方法。
  • done()
  • finally()
  • Promise.try(),統一同步異步寫法,且讓同步的同步執行,異步的異步執行,bluebird提供了這個方法
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next

Generator

  • Generator 函數是一個狀態機,封裝了多個內部狀態。
  • 執行 Generator 函數會返回一個遍歷器對象,其可以依次遍歷 Generator 函數內部的每一個狀態。
  • 調用 Generator 函數后,該函數并不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,代表 Generator 函數的內部指針,也就是遍歷器對象(Iterator Object),指向該函數的內部狀態。必須調用遍歷器對象的next方法,使得指針移向下一個狀態。
  • yield表達式就是暫停標志
  • 任意一個對象的Symbol.iterator方法,等于該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。由于 Generator 函數就是遍歷器生成函數,因此可以把 Generator 賦值給對象的Symbol.iterator屬性,從而使得該對象具有 Iterator 接口。
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
  yield 1
  yield 2
  yield 3
}
[...myIterable] // [1, 2, 3]
  • next()方法,可以帶一個參數,該參數就會被當作上一個yield表達式的返回值。而yield表達式本身沒有返回值,或者說總是返回undefined
  • Generator 函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。通過next方法的參數,就有辦法在 Generator 函數開始運行之后,繼續向函數體內部注入值。也就是說,可以在 Generator 函數運行的不同階段,從外部向內部注入不同的值,從而調整函數行為
  • 由于next方法的參數表示上一個yield表達式的返回值,所以在第一次使用next方法時,傳遞參數是無效的。V8 引擎直接忽略第一次使用next方法時的參數,只有從第二次使用next方法開始,參數才是有效的。從語義上講,第一個next方法用來啟動遍歷器對象,所以不用帶有參數
  • for...of可以直接遍歷Generator,且此時不再需要調用next方法。
  • 原生對象加上加上遍歷器接口就可以用for...of遍歷
function* objectEntries() {
  let propKeys = Object.keys(this);
  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
  • Generator.prototype.throw(),可以在函數體外拋出錯誤,然后在 Generator 函數體內捕獲。
  • 一旦 Generator 執行過程中拋出錯誤,且沒有被內部捕獲,就不會再執行下去了。如果此后還調用next方法,將返回一個value屬性等于undefined、done屬性等于true的對象,即 JavaScript 引擎認為這個 Generator 已經運行結束了。
  • Generator.prototype.return(),可以返回給定的值,并且終結遍歷 Generator 函數。
  • next()、throw()、return() 的共同點:
    1 next()是將yield表達式替換成一個值。(undefined)
    2 throw()是將yield表達式替換成一個throw語句。
    3 return()是將yield表達式替換成一個return語句。
  • yield* 表達式,用來在一個 Generator 函數里面執行另一個 Generator 函數。
  • 如果yield表達式后面跟的是一個遍歷器對象,需要在yield表達式后面加上星號,表明它返回的是一個遍歷器對象
  • 任何數據結構只要有 Iterator 接口,就可以被yield*遍歷
let read = (function* () {
  yield 'hello'
  yield* 'hello'
})()
read.next().value // "hello"
read.next().value // "h"
  • yield*命令可以很方便地取出嵌套數組的所有成員
function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e
  • 讓 Generator 函數返回一個正常的對象實例,既可以用next方法,又可以獲得正常的this
function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);
f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
function F() {
  return gen.call(gen.prototype);
}
var f = new F();
f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
  • Generator 函數是 ES6 對協程的實現,可以將多個需要互相協作的任務寫成 Generator 函數,它們之間使用yield表示式交換控制權。
  • 利用 Generator 函數,可以在任意對象上部署 Iterator 接口
  • Generator 可以看作是一個數組結構,因為 Generator 函數可以返回一系列的值,這意味著它可以對任意表達式,提供類似數組的接口。
  • 協程
    1 第一步,協程A開始執行
    2 第二步,協程A執行到一半,進入暫停,執行權轉移到協程B。
    3 第三步,(一段時間后)協程B交還執行權。
    4 第四步,協程A恢復執行。
function* asyncJob() {
  // ...其他代碼
  var f = yield readFile(fileA);
  // ...其他代碼
}

上面代碼的函數asyncJob是一個協程,它的奧妙就在其中的yield命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield命令是異步兩個階段的分界線。協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續往后執行。它的最大優點,就是代碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。

  • Thunk 函數的含義,編譯器的“傳名調用”實現,往往是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫做 Thunk 函數。在JS中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數作為參數的單參數函數。
// 正常版本的readFile(多參數版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(單參數版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);//函數的2次調用
  • Thunk的意義在于Generator的自動執行,通過回調函數
function run(fn) {
  var gen = fn();
  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);//Thunk函數
  }
  next()
}
function* g() {
  // ...
}
run(g)
  • co,co函數返回一個Promise對象,因此可以用then方法添加回調函數,用于Generator的自動執行,使用 co 的前提條件是,yield命令后面,只能是 Thunk 函數或 Promise 對象。如果數組或對象的成員,全部都是 Promise 對象,也可以使用 co。
function run(gen){
  var iterator = gen()
  function next(data){
    var result = iterator.next(data)
    if (result.done) return result.value
    result.value.then(function(data){//Promise
      next(data)
    });
  }
  next()
}
run(gen)
  • co處理并發的異步操作,要把并發的操作都放在數組或對象里面,跟在yield語句后面。
// 數組的寫法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);
// 對象的寫法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  }
  console.log(res)
}).catch(onerror)

async

  • 內置執行器,Generator 函數需要next(),或CO
  • await命令后面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同于同步操作)。
async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}
async function asyncPrint(value, ms) {
  await timeout(ms)
  console.log(value)
}
asyncPrint('hello world', 50)
  • 返回值是 Promise,可以用Then()
  • async函數內部return語句返回的值,會成為then方法回調函數的參數
  • async函數執行立刻返回Promise,其狀態必須等到內部所有await命令后面的 Promise 對象執行完,才會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操作執行完,才會執行then方法指定的回調函數。
  • 前一個異步操作失敗,也不會中斷后面的異步操作,有下面兩種操作。
async function f() {
  try {
    await Promise.reject('出錯了')
  } catch(e) {
  }
  return await Promise.resolve('hello world')
}
f()
.then(v => console.log(v))
// hello world
async function f() {
  await Promise.reject('出錯了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出錯了
// hello world
  • 多個await命令后面的異步操作,如果不存在繼發關系,最好讓它們同時觸發。
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 寫法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
  • async 函數的實現原理,將 Generator 函數和自動執行器,包裝在一個函數里。
  • for await...of,用于遍歷異步的Iterator 接口

單線程和異步

瀏覽器只能做一件事——DOM渲染,為了避免DOM重復渲染,瀏覽器只提供了一個線程,即是一個單線程實例,類似于強計算、重IO的操作將使線程阻塞,視覺效果即為“卡住了”,因此就有了異步的出現。
異步是解決單線程的唯一方案,但是對于coder而言,callback函數執行順序并不可控(定時器函數、Ajax請求等),callback函數不容易模塊化且可讀性十分差。異步的原理如下:

event-loop

  1. 同步代碼直接執行;
  2. 異步代碼的回調函數先放在異步隊列中(將要執行的時候放入)
  3. 待同步代碼執行完畢后,輪詢執行異步隊列的函數。

JQuery Deferred

//Deferred使用方法
function waitHandle(){
  var dtd = $.Deferred()//生成Deferred對象實例
  var wait = function( dtd ){
    //對dtd對象深加工,即異步操作
    //異步操作后
    if(success){
      dtd.resolve()
    } else {
      dtd.reject()
    } 
    return dtd
  }
  return wait( dtd )//將深加工后的dtd對象返回
}

//調用
var w = waitHandle()
w
  .then(func1, func2)//成功和失敗的回調
  .then(func3, func4)//成功和失敗的回調

dtd的API分為兩類:resolve、rejectthen、done、fail,由于這兩類方法分別表示因果,因此需要一定的手段將其強制分離使用,但是上文的dtd對象并不能夠(可以直接使用dtd.reject()修改resolve狀態),因此有了Promise對象,其僅僅提供then等結果方法,避免了外部暴力修改dtd狀態的可能。將上文代碼段進行如下修改:

......
return dtd.promise()//返回promise對象
  }
  return wait( dtd )
}
......

class

  • 用以取代prototype,構造方法為constructor方法,也用new生成對象實例,實際上為ES5的語法糖
class A {

}

typeof A //"function"
A === A.prototype.constructor //true
a.__proto__ === A.prototype //true
  • 類的方法都定義在prototype對象上面,所以類的新方法可以添加在prototype對象上面。Object.assign方法可以很方便地一次向類添加多個方法。
class Point {
  constructor(){
  }
}
Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
})
  • constructor(),類的默認方法,通過new命令生成對象實例時,自動調用該方法,默認返回實例對象(即this),指定return返回另外一個對象
  • 與 ES5 一樣,類的所有實例共享一個原型對象
  • Class 表達式,下面為一個立即執行的類的實例
let person = new class {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name)
  }
}('張三');
person.sayName(); // "張三"
  • 私有方法
    1 通過把方法移到模塊外部
class Widget {
  foo (baz) {
    bar.call(this, baz);
  }
  // ...
}
function bar(baz) {
  return this.snaf = baz;
}

2 利用Symbol值的唯一性,將私有方法的名字命名為一個Symbol值。

const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
  // 公有方法
  foo(baz) {
    this[bar](baz);
  }
  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }
  // ...
};
  • this 的指向,默認指向類的實例,單獨使用會引起上下文混亂,可以通過在構造函數中使用bind(this)、剪頭函數和proxy綁定this指向
  • Class 的靜態方法,加上static關鍵字,就表示該方法不會被實例繼承,而是直接通過類來調用,如果靜態方法包含this關鍵字,這個this指的是類,而不是實例。
  • Class 的靜態屬性和實例屬性
class MyClass {
  (static) myProp = 42;
  constructor() {
    console.log(this.myProp); // 42
  }
}

myProp就是MyClass的實例屬性(加上static為靜態屬性)。在MyClass的實例上,可以讀取這個屬性,不用必須寫在constructor()中,在React中可讀性更強

class ReactCounter extends React.Component {
  state = {
    count: 0
  };
}
  • new.target 屬性,ES6 為new命令引入了一個new.target屬性,該屬性一般用在構造函數之中,返回new命令作用于的那個構造函數。如果構造函數不是通過new命令調用的,new.target會返回undefined,因此這個屬性可以用來確定構造函數是怎么調用的。
  • extends,對象繼承
  • super,用來創建父類的this,子類必須在constructor()方法中調用super方法,否則新建實例時會報錯。這是因為子類沒有自己的this對象,而是繼承父類的this對象,然后對其進行加工。如果不調用super方法,子類就得不到this對象
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 調用父類的constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // 調用父類的toString()
  }
}
  • super作為函數,代表父類的構造函數。ES6 要求,子類的構造函數必須執行一次super函數。但是super內部的this指的是子類實例
class A {}
class B extends A {
  constructor() {
    super();
  }
}
  • super作為對象,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類。
class A {
  p() {
    return 2;
  }
}
class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2,A,指向A.prototype
  }
}
let b = new B()
  • this指向子類,所以如果通過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類實例的屬性
  • ES6 的繼承機制是先創造父類的實例對象this(所以必須先調用super方法),然后再用子類的構造函數修改this。
  • 在子類的構造函數中,只有調用super之后,才可以使用this關鍵字,否則會報錯。這是因為子類實例的構建,是基于對父類實例加工,只有super方法才能返回父類實例。
  • Object.getPrototypeOf(),用來從子類上獲取父類
  • prototype屬性和proto屬性這兩條繼承鏈。
  • 1 子類的proto屬性,表示構造函數的繼承,總是指向父類。
  • 2 子類prototype屬性的proto屬性,表示方法的繼承,總是指向父類的prototype屬性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
  • 實例的 proto 屬性,通過子類實例的proto.proto屬性,可以修改父類實例的行為
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
p2.__proto__.__proto__.printName = function () {
  console.log('Ha');
};
p1.printName() // "Ha"
  • 原生構造函數(用于生成數據結構)的繼承
    Boolean()
    Number()
    String()
    Array()
    Date()
    Function()
    RegExp()
    Error()
    Object()
    ES6先構造父類的this,然后通過子類的this繼承父類的行為,這樣可以生成構造函數的子類
  • Mixin 模式的實現,指的是多個對象合成一個新的對象,新對象具有各個組成成員的接口

JS構造函數

function A(x, y){
  this.x = x
  this.y = y
}//JS構造函數的寫法
A.prototype.add = function(){
  return this.x + this.y
}//通過prototype對象擴展構造函數的方法
var a = new A(1, 2)//同樣通過new來生成對象實例

繼承通過prototype來實現

function A(){}
function B(){}
B.prototype = new A() 
var b = new B() //B繼承A

Zepto中的原型源碼

(function(window){
  var zepto = {}

  Z.prototype = $.fn

  $.fn = {//通過這個來擴展原型方法
    css: function(){},
    html: function(){},
    ......
  }

  function Z(dom, selector){
    var i, len = dom? dom.length: 0
    for(i=0; i < len; i++){
      this[i] = dom[i]
    }
    this.length = len
    this.selector = selector || ''
  }

  zepto.Z = function(dom, selector){
    return new Z(dom, selector)
  }

  zepto.init = function(selector){
    var slice = Array.prototype.slice
    var dom = slice.call(document.quertSelectorAll(selector))//將類數組轉為數組
    return zepto.Z(dom, selector)
  }

  var $ = function(selector){
    return zepto.init(selector)
  }

  window.$ = $
})(window)//自執行函數避免全局變量污染

JQuery中的原型實現與Zepto十分形似。

(function(window){
  var jQuery = function(selector){
    return new jQuery.fn.init(selector)
  }

  jQuery.fn = {}

  var init = jQuery.fn.init = function(selector){
    var slice = Array.prototype.slice
    var dom = slice.call(document.quertSelectorAll(selector))

    var i, len = dom? dom.length: 0
    for(i=0; i < len; i++){
      this[i] = dom[i]
    }
    this.length = len
    this.selector = selector || ''
  }
  init.prototype = jQuery.fn

  jQuery.fn = {
    css: function(){},
    html: function(){},
    ......
  }
  window.$ = jQuery
})(window)//自執行函數避免全局變量污染

call(),apply()和bind()

  • call()和apply()用來改變this,就是調用函數,讓它在指定的上下文執行,這樣,函數可以訪問的作用域就會改變。
  • Function對象的方法
  • bind()也用于上下文綁定,其新創建一個函數,然后把它的上下文綁定到bind()括號中的參數上,然后將它返回——bind后函數不會執行,而只是返回一個改變了上下文的函數副本,而call和apply是直接執行函數。
if (!function() {}.bind) {
    Function.prototype.bind = function(context) {
        var self = this
            , args = Array.prototype.slice.call(arguments);
            
        return function() {
            return self.apply(context, args.slice(1));    
        }
    };
}

裝飾器

  • 是一個函數,用來修改類的行為,這個函數的第一個參數,就是所要修飾的目標類。
@testable
class MyTestableClass {
  // ...
}
function testable(target) {
  target.isTestable = true;
}
MyTestableClass.isTestable // true

上面代碼中,@testable就是一個裝飾器。它修改了MyTestableClass這個類的行為,為它加上了靜態屬性isTestable。testable函數的參數target是MyTestableClass類本身。

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false

上面代碼中,裝飾器testable可以接受參數,這就等于可以修改裝飾器的行為

  • 裝飾器對類的行為的改變,是代碼編譯時發生的,而不是在運行時。這意味著,裝飾器能在編譯階段運行代碼。也就是說,裝飾器本質就是編譯時執行的函數。
  • 可以修飾類的屬性
class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}//裝飾器readonly用來修飾“類”的name方法
function readonly(target, name, descriptor){
  // descriptor對象原來的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 類似于
Object.defineProperty(Person.prototype, 'name', descriptor);

裝飾器函數一共可以接受三個參數,第一個參數是所要修飾的目標對象,即類的實例(這不同于類的修飾,那種情況時target參數指的是類本身);第二個參數是所要修飾的屬性名,第三個參數是該屬性的描述對象。

裝飾器的應用場景之一:路由

當項目過大時,API也隨著變得更加復雜和難以維護,這時,可以根據應用場景拆分路由,使得路由和應用場景一一對應。

Module

ES6之前,為了應付大型項目的開發,社區制定了對應瀏覽器端的ADM(CMD)標準,對應服務器端的CommonJS標準。

// CommonJS模塊
let { stat, exists, readFile } = require('fs')
// 等同于
let _fs = require('fs')
let stat = _fs.stat
let exists = _fs.exists
let readfile = _fs.readfile

上面代碼的實質是整體加載fs模塊(即加載fs的所有方法),生成一個對象(_fs),然后再從這個對象上面讀取 3 個方法。這種加載稱為“運行時加載”,因為只有運行時才能得到這個對象,導致完全沒辦法在編譯時做“靜態優化”。
直到ES6出現后,ES6 模塊的設計思想是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。CommonJSAMD 模塊,都只能在運行時確定這些東西。

// ES6模塊
import { stat, exists, readFile } from 'fs'

上面代碼的實質是從fs模塊加載 3 個方法,其他方法不加載。這種加載稱為“編譯時加載”或者靜態加載,即 ES6 可以在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。當然,這也導致了沒法引用 ES6 模塊本身,因為它不是對象。

export

  • export的寫法
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};
  • export還可以輸出類和方法
export function multiply(x, y) {
  return x * y;
}
  • export輸出的變量可以使用as關鍵字重命名
  • 接口名與模塊內部變量之間需要建立一一對應的關系
// 寫法一
export var m = 1

// 寫法二
var m = 1
export {m}

// 寫法三
var n = 1
export {n as m}

import

  • import寫法
// main.js
import {firstName, lastName, year} from './profile.js'

大括號里面的變量名,必須與被導入模塊profile.js對外接口的名稱相同。

  • import命令輸入的變量都是只讀的,因為它的本質是輸入接口。也就是說,不允許在加載模塊的腳本里面,改寫接口。
  • import命令是編譯階段執行的,在代碼運行之前,因此其有提升的效果。
  • 可以用星號*指定一個對象,所有輸出值都加載在這個對象上面

整體加載的寫法如下:

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
import * as circle from './circle';

console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));

模塊整體加載所在的那個對象(上例是circle),應該是可以靜態分析的,所以不允許運行時改變。

  • export default
    importexport default導出的模塊時,可以任意指定這個模塊導出的方法,這時import命令后面,不使用大括號。一個模塊只有一個默認輸出。
  • export default命令其實只是輸出一個叫做default的變量,所以它后面不能跟變量聲明語句。
// 錯誤
export default var a = 1
  • exportimport 的復合寫法:
export { foo, bar } from 'my_module'

// 可以簡單理解為
import { foo, bar } from 'my_module'
export { foo, bar }

寫成一行以后,foobar實際上并沒有被導入當前模塊,只是相當于對外轉發了這兩個接口,導致當前模塊不能直接使用foobar

瀏覽器異步加載

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

上面代碼中,<script>標簽打開deferasync屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行后面的命令。
deferasync的區別是:defer要等到整個頁面在內存中正常渲染結束(DOM 結構完全生成,以及其他腳本執行完成),才會執行;async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本以后,再繼續渲染。一句話,defer是“渲染完再執行”,async是“下載完就執行”。另外,如果有多個defer腳本,會按照它們在頁面出現的順序加載,而多個async腳本是不能保證加載順序的。
瀏覽器加載 ES6 模塊,也使用<script>標簽,但是要加入type="module"屬性。

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

ES6 模塊與 CommonJS 模塊的差異

  1. CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
    CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值:
// lib.js
var counter = 3
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
}
// main.js
var mod = require('./lib')

console.log(mod.counter)  // 3
mod.incCounter()
console.log(mod.counter) // 3

上面代碼說明,lib.js模塊加載以后,它的內部變化就影響不到輸出的mod.counter了。這是因為mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能得到內部變動后的值:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },//輸出的counter屬性實際上是一個取值器函數
  incCounter: incCounter,
}

JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的import有點像 Unix 系統的“符號連接”,原始值變了,import加載的值也會跟著變。因此,ES6 模塊是動態引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊,ES6 模塊輸入的變量是活的,完全反應其所在模塊內部的變化。

  1. CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口

node中的importexport

  • Node 對 ES6 模塊的處理比較麻煩,因為它有自己的 CommonJS 模塊格式,與 ES6 模塊格式是不兼容的。目前的解決方案是,將兩者分開,ES6 模塊和 CommonJS 采用各自的加載方案。
  • ES6 模塊之中,頂層的this指向undefinedCommonJS模塊的頂層this指向當前模塊,這是兩者的一個重大差異。
  • ES6 模塊加載 CommonJS 模塊時,CommonJS模塊的輸出都定義在module.exports這個屬性上面。Node 的import命令加載 CommonJS模塊,Node 會自動將module.exports屬性,當作模塊的默認輸出,即等同于export default xxx
  • ES6 模塊加載 CommonJS 模塊,不能使用require命令,而要使用import()函數。ES6 模塊的所有輸出接口,會成為輸入對象的屬性。

循環加載

  1. CommonJS 模塊的循環加載:
    CommonJS 的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,然后在內存生成一個對象。以后需要用到這個模塊的時候,就會到exports屬性上面取值。即使再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊無論加載多少次,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結果,除非手動清除系統緩存。
    CommonJS 模塊的重要特性是加載時執行,即腳本代碼在require的時候,就會全部執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。
  2. ES6 模塊的循環加載:
    ES6 處理“循環加載”與 CommonJS 有本質的不同。ES6 模塊是動態引用,如果使用import從一個模塊加載變量import foo from 'foo',那些變量不會被緩存,而是成為一個指向被加載模塊的引用,需要開發者自己保證,真正取值的時候能夠取到值,通常可通過函數的返回值解決,因為函數具有提升效果(函數表達式并沒有提升效果)。

附錄

JS中不常用方法集合

reduce()方法

此為JS高階方法,為累加器函數,對數組中的元素從左至右分別作用于callbackfn,并返回一個“加工”后的元素。

array1.reduce(callbackfn[, initialValue])

模塊化總結

模塊化是現代前端架構的方向。從模塊化概念誕生之初,AMD首先成為標準(通過require.js庫,淘寶開源過CMD);后來緊接著出現了各種前端打包工具,使得后端的common.js標準(一個文件就是一個模塊,擁有單獨的作用域;普通方式定義的變量、函數、對象都屬于該模塊內;通過require來加載模塊;通過exports和module.exports來暴露模塊中的內容)可以被前端所用;直到ES6標準的出現,node端基本支持,但由于瀏覽器的支持有限,所以需要使用babel來進行語法轉化才可以放心大膽使用。
模塊化基本語法如下:

//util.js中:
export default { a: 100}
export function fn1() { alert('1')} 
export function fn2() { alert('2')}

import util from './util.js'
import {fn1, fn2} from './util.js'

目前比較火的模塊化工具當屬webpack,它的功能異常強大,但下面將介紹另一種打包工具,React和Vue都是由它打包的:

rollup

rollup雖然功能單一,但是其把打包做到極致(代碼更少、體積更小),因此利于繼承和擴展,而webpack則功能強大。

類型判斷

作用域

引用傳遞

  • JS中對象是引用傳遞,非對象(Undefined,Null,Boolean,Number,String)是值傳遞,但是通過將非對象進行包裝,也可以進行引用傳遞
  • Boolean,Number,String有各自的包裝對象
null == undefined 
#true,傳值
[1] == [2]
#false,傳遞引用

內存泄漏

  1. 全局變量
a = 10;
//未聲明對象。
global.b = 11;
//全局變量引用

這種比較簡單的原因,全局變量直接掛在 root 對象上,不會被清除掉。

  1. 閉包
function out() {
  const bigData = new Buffer(100);
  inner = function () {
    void bigData;
  }
}

inner 直接掛在了 root 上,從而導致內存泄漏(bigData 不會釋放)。

  1. 事件監聽
    對同一個事件重復監聽,忘記移除(removeListener),將造成內存泄漏。這種情況很容易在復用對象上添加事件時出現。
  2. 緩存
    在使用緩存的時候,得清楚緩存的對象的多少,如果緩存對象非常多,得做限制最大緩存數量處理。還有就是非常占用 CPU 的代碼也會導致內存泄漏,服務器在運行的時候,如果有高 CPU 的同步代碼,因為Node.js 是單線程的,所以不能處理處理請求,請求堆積導致內存占用過高。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 在此處先列下本篇文章的主要內容 簡介 next方法的參數 for...of循環 Generator.prototy...
    醉生夢死閱讀 1,459評論 3 8
  • [TOC] 參考阮一峰的ECMAScript 6 入門參考深入淺出ES6 let和const let和const都...
    郭子web閱讀 1,800評論 0 1
  • 簡介 基本概念 Generator函數是ES6提供的一種異步編程解決方案,語法行為與傳統函數完全不同。本章詳細介紹...
    呼呼哥閱讀 1,084評論 0 4
  • 含義 async函數是Generator函數的語法糖,它使得異步操作變得更加方便。 寫成async函數,就是下面這...
    oWSQo閱讀 2,000評論 0 2
  • 一、let 和 constlet:變量聲明, const:只讀常量聲明(聲明的時候賦值)。 let 與 var 的...
    dadage456閱讀 766評論 0 0