本文為阮一峰大神的《ECMAScript 6 入門》的個人版提純!
babel
babel負責將JS高級語法轉義,使之能被各種瀏覽器所執行。其使用步驟如下:
- 編寫配置文件
.babelrc
此文件存于根目錄,基本格式如下:
{
"presets": [],
"plugins": []
}
- 選定轉碼規則
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
- 將這些規則加入
.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
,然后聲明了變量y
,y
的默認值是一個匿名函數。這個匿名函數內部的變量x
,指向同一個作用域的第一個參數x
。函數foo
內部又聲明了一個內部變量x
,該變量與第一個參數x
由于不是同一個作用域,所以不是同一個變量,因此執行y
后,內部變量x
和外部全局變量x
的值都沒變。如果將var x = 3
的var
去除,函數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()
代替,為了規避掉繼承的屬性 - 屬性的遍歷
-
for...in
,循環遍歷對象自身的和繼承的可枚舉屬性(不含Symbol
屬性)。 -
Object.keys(obj)
,返回一個數組,包括對象自身的(不含繼承的)所有可枚舉屬性(不含Symbol
屬性)的鍵名。 -
Object.getOwnPropertyNames(obj)
,返回一個數組,包含對象自身的所有屬性(不含Symbol
屬性,但是包括不可枚舉屬性)的鍵名。 -
Object.getOwnPropertySymbols(obj)
,返回一個數組,包含對象自身的所有Symbol
屬性的鍵名。 -
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()
- 深拷貝
JSON.parse(JSON.stringify(initalObj))
- 遞歸
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 個內置函數
1Symbol.hasInstance
2Symbol.isConcatSpreadable
3Symbol.species
4Symbol.match
5Symbol.replace
6Symbol.search
7Symbol.split
8Symbol.iterator
9Symbol.toPrimitive
10Symbol.toStringTag
11Symbol.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
對象上。現階段,某些方法同時在Object
和Reflect
對象上部署,未來的新方法將只部署在Reflect
對象上。也就是說,從Reflect
對象上可以拿到語言內部的方法 - 修改某些
Object
方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)
在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)
則會返回false
- 讓
Object
操作都變成函數行為。某些Object
操作是命令式,比如name in obj
和delete 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的遍歷操作
1keys()
:返回鍵名的遍歷器,keys方法和values方法的行為完全一致
2values()
:返回鍵值的遍歷器,keys方法和values方法的行為完全一致
3entries()
:返回鍵值對的遍歷器
4forEach()
:使用回調函數遍歷每個成員 -
WeakSet
與Set
類似,成員不重復,但只能是對象。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
構造函數的參數
1size
屬性
2set(key, value)
3get(key)
4has(key)
,返回一個布爾值
5delete(key)
,返回true
6clear()
,清除所有對象,無返回值 - 遍歷方法,Map 的遍歷順序就是插入順序
1keys()
:返回鍵名的遍歷器。
2values()
:返回鍵值的遍歷器。
3entries()
:返回所有成員的遍歷器。
4forEach()
:遍歷 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:
1Array
2Map
3Set
4String
5TypedArray
6 函數的arguments
對象
7NodeList
對象
Promise
- 一個保存異步結果的容器
- 兩種狀態:
pedding => resolved
;pedding => 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
- 同步代碼直接執行;
- 異步代碼的回調函數先放在異步隊列中(將要執行的時候放入)
- 待同步代碼執行完畢后,輪詢執行異步隊列的函數。
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、reject
和then、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 模塊的設計思想是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。CommonJS
和 AMD
模塊,都只能在運行時確定這些東西。
// 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
命令輸入的變量都是只讀的,因為它的本質是輸入接口。也就是說,不允許在加載模塊的腳本里面,改寫接口。 -
impor
t命令是編譯階段執行的,在代碼運行之前,因此其有提升的效果。 - 可以用星號
*
指定一個對象,所有輸出值都加載在這個對象上面
整體加載的寫法如下:
// 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
當import
由export default
導出的模塊時,可以任意指定這個模塊導出的方法,這時import
命令后面,不使用大括號。一個模塊只有一個默認輸出。 -
export default
命令其實只是輸出一個叫做default
的變量,所以它后面不能跟變量聲明語句。
// 錯誤
export default var a = 1
-
export
與import
的復合寫法:
export { foo, bar } from 'my_module'
// 可以簡單理解為
import { foo, bar } from 'my_module'
export { foo, bar }
寫成一行以后,foo
和bar
實際上并沒有被導入當前模塊,只是相當于對外轉發了這兩個接口,導致當前模塊不能直接使用foo
和bar
。
瀏覽器異步加載
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代碼中,<script>
標簽打開defer
或async
屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行后面的命令。
defer
與async
的區別是: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
模塊的差異
-
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 模塊輸入的變量是活的,完全反應其所在模塊內部的變化。
- CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口
node中的
import
與export
- Node 對 ES6 模塊的處理比較麻煩,因為它有自己的
CommonJS
模塊格式,與 ES6 模塊格式是不兼容的。目前的解決方案是,將兩者分開,ES6 模塊和 CommonJS 采用各自的加載方案。 - ES6 模塊之中,頂層的
this
指向undefined
;CommonJS
模塊的頂層this
指向當前模塊,這是兩者的一個重大差異。 - ES6 模塊加載
CommonJS
模塊時,CommonJS
模塊的輸出都定義在module.exports
這個屬性上面。Node 的import
命令加載CommonJS
模塊,Node 會自動將module.exports
屬性,當作模塊的默認輸出,即等同于export default xxx
。 - ES6 模塊加載
CommonJS
模塊,不能使用require
命令,而要使用import()
函數。ES6 模塊的所有輸出接口,會成為輸入對象的屬性。
循環加載
- CommonJS 模塊的循環加載:
CommonJS 的一個模塊,就是一個腳本文件。require
命令第一次加載該腳本,就會執行整個腳本,然后在內存生成一個對象。以后需要用到這個模塊的時候,就會到exports
屬性上面取值。即使再次執行require
命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊無論加載多少次,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結果,除非手動清除系統緩存。
CommonJS 模塊的重要特性是加載時執行,即腳本代碼在require
的時候,就會全部執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。 - 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,傳遞引用
內存泄漏
- 全局變量
a = 10;
//未聲明對象。
global.b = 11;
//全局變量引用
這種比較簡單的原因,全局變量直接掛在 root 對象上,不會被清除掉。
- 閉包
function out() {
const bigData = new Buffer(100);
inner = function () {
void bigData;
}
}
inner 直接掛在了 root 上,從而導致內存泄漏(bigData 不會釋放)。
- 事件監聽
對同一個事件重復監聽,忘記移除(removeListener),將造成內存泄漏。這種情況很容易在復用對象上添加事件時出現。 - 緩存
在使用緩存的時候,得清楚緩存的對象的多少,如果緩存對象非常多,得做限制最大緩存數量處理。還有就是非常占用 CPU 的代碼也會導致內存泄漏,服務器在運行的時候,如果有高 CPU 的同步代碼,因為Node.js 是單線程的,所以不能處理處理請求,請求堆積導致內存占用過高。