ES6 容許直接寫入變量和函數,做爲對象的屬性和方法。這樣的書寫更加簡潔。git
1 const foo = 'bar'; 2 const baz = {foo}; 3 baz // {foo: "bar"} 4 5 // 等同於 6 const baz = {foo: foo};
上面代碼代表,ES6 容許在對象之中,直接寫變量。這時,屬性名爲變量名, 屬性值爲變量的值。下面是另外一個例子。github
1 function f(x, y) { 2 return {x, y}; 3 } 4 5 // 等同於 6 7 function f(x, y) { 8 return {x: x, y: y}; 9 } 10 11 f(1, 2) // Object {x: 1, y: 2}
除了屬性簡寫,方法也能夠簡寫。編程
1 const o = { 2 method() { 3 return "Hello!"; 4 } 5 }; 6 7 // 等同於 8 9 const o = { 10 method: function() { 11 return "Hello!"; 12 } 13 };
下面是一個實際的例子。app
1 let birth = '2000/01/01'; 2 3 const Person = { 4 5 name: '張三', 6 7 //等同於birth: birth 8 birth, 9 10 // 等同於hello: function ()... 11 hello() { console.log('個人名字是', this.name); } 12 13 };
這種寫法用於函數的返回值,將會很是方便。函數式編程
1 function getPoint() { 2 const x = 1; 3 const y = 10; 4 return {x, y}; 5 } 6 7 getPoint() 8 // {x:1, y:10}
CommonJS 模塊輸出一組變量,就很是合適使用簡潔寫法。函數
1 let ms = {}; 2 3 function getItem (key) { 4 return key in ms ? ms[key] : null; 5 } 6 7 function setItem (key, value) { 8 ms[key] = value; 9 } 10 11 function clear () { 12 ms = {}; 13 } 14 15 module.exports = { getItem, setItem, clear }; 16 // 等同於 17 module.exports = { 18 getItem: getItem, 19 setItem: setItem, 20 clear: clear 21 };
屬性的賦值器(setter)和取值器(getter),事實上也是採用這種寫法。工具
1 const cart = { 2 _wheels: 4, 3 4 get wheels () { 5 return this._wheels; 6 }, 7 8 set wheels (value) { 9 if (value < this._wheels) { 10 throw new Error('數值過小了!'); 11 } 12 this._wheels = value; 13 } 14 }
注意,簡潔寫法的屬性名老是字符串,這會致使一些看上去比較奇怪的結果。優化
1 const obj = { 2 class () {} 3 }; 4 5 // 等同於 6 7 var obj = { 8 'class': function() {} 9 };
ES6 容許字面量定義對象時,用方法二(表達式)做爲對象的屬性名,即把表達式放在方括號內。this
1 let propKey = 'foo'; 2 3 let obj = { 4 [propKey]: true, 5 ['a' + 'bc']: 123 6 };
下面是另外一個例子spa
1 let lastWord = 'last word'; 2 3 const a = { 4 'first word': 'hello', 5 [lastWord]: 'world' 6 }; 7 8 a['first word'] // "hello" 9 a[lastWord] // "world" 10 a['last word'] // "world"
表達式還能夠用於定義方法名。
1 let obj = { 2 ['h' + 'ello']() { 3 return 'hi'; 4 } 5 }; 6 7 obj.hello() // hi
注意,屬性名錶達式與簡潔表示法,不能同時使用,會報錯。
1 // 報錯 2 const foo = 'bar'; 3 const bar = 'abc'; 4 const baz = { [foo] }; 5 6 // 正確 7 const foo = 'bar'; 8 const baz = { [foo]: 'abc'};
注意,屬性名錶達式若是是一個對象,默認狀況下會自動將對象轉爲字符串[object Object]
,這一點要特別當心。
1 const keyA = {a: 1}; 2 const keyB = {b: 2}; 3 4 const myObject = { 5 [keyA]: 'valueA', 6 [keyB]: 'valueB' 7 }; 8 9 myObject // Object {[object Object]: "valueB"}
上面代碼中,[keyA]
和[keyB]
獲得的都是[object Object]
,因此[keyB]
會把[keyA]
覆蓋掉,而myObject
最後只有一個[object Object]
屬性。
函數的name
屬性,返回該函數的函數名。
1 function foo() {} 2 foo.name // "foo"
須要注意的是,ES6 對這個屬性的行爲作出了一些修改。若是將一個匿名函數賦值給一個變量,ES5 的name
屬性,會返回空字符串,而 ES6 的name
屬性會返回實際的函數名。
1 var f = function () {}; 2 3 // ES5 4 f.name // "" 5 6 // ES6 7 f.name // "f"
若是將一個具名函數賦值給一個變量,則 ES5 和 ES6 的name
屬性都返回這個具名函數本來的名字。
1 const bar = function baz() {}; 2 3 // ES5 4 bar.name // "baz" 5 6 // ES6 7 bar.name // "baz"
Function
構造函數返回的函數實例,name
屬性的值爲anonymous
。
1 (new Function).name // "anonymous"
bind
返回的函數,name
屬性值會加上bound
前綴。
1 function foo() {}; 2 foo.bind({}).name // "bound foo" 3 4 (function(){}).bind({}).name // "bound "
ES6 容許使用「箭頭」(=>
)定義函數。
1 var f = v => v; 2 3 // 等同於 4 var f = function (v) { 5 return v; 6 };
若是箭頭函數不須要參數或須要多個參數,就使用一個圓括號表明參數部分。
1 var f = () => 5; 2 // 等同於 3 var f = function () { return 5 }; 4 5 var sum = (num1, num2) => num1 + num2; 6 // 等同於 7 var sum = function(num1, num2) { 8 return num1 + num2; 9 };
若是箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,而且使用return
語句返回。
1 var sum = (num1, num2) => { return num1 + num2; }
因爲大括號被解釋爲代碼塊,因此若是箭頭函數直接返回一個對象,必須在對象外面加上括號,不然會報錯。
1 // 報錯 2 let getTempItem = id => { id: id, name: "Temp" }; 3 4 // 不報錯 5 let getTempItem = id => ({ id: id, name: "Temp" });
下面是一種特殊狀況,雖然能夠運行,但會獲得錯誤的結果
1 let foo = () => { a: 1 }; 2 foo() // undefined
上面代碼中,原始意圖是返回一個對象{ a: 1 }
,可是因爲引擎認爲大括號是代碼塊,因此執行了一行語句a: 1
。這時,a
能夠被解釋爲語句的標籤,所以實際執行的語句是1;
,而後函數就結束了,沒有返回值。
若是箭頭函數只有一行語句,且不須要返回值,能夠採用下面的寫法,就不用寫大括號了。
1 let fn = () => void doesNotReturn();
箭頭函數能夠與變量解構結合使用。
1 const full = ({ first, last }) => first + ' ' + last; 2 3 // 等同於 4 function full(person) { 5 return person.first + ' ' + person.last; 6 }
箭頭函數使得表達更加簡潔。
1 const isEven = n => n % 2 == 0; 2 const square = n => n * n;
上面代碼只用了兩行,就定義了兩個簡單的工具函數。若是不用箭頭函數,可能就要佔用多行,並且還不如如今這樣寫醒目。
箭頭函數的一個用處是簡化回調函數。
1 // 正常函數寫法 2 [1,2,3].map(function (x) { 3 return x * x; 4 }); 5 6 // 箭頭函數寫法 7 [1,2,3].map(x => x * x);
另外一個例子是
1 // 正常函數寫法 2 var result = values.sort(function (a, b) { 3 return a - b; 4 }); 5 6 // 箭頭函數寫法 7 var result = values.sort((a, b) => a - b);
下面是 rest 參數與箭頭函數結合的例子。
1 const numbers = (...nums) => nums; 2 3 numbers(1, 2, 3, 4, 5) 4 // [1,2,3,4,5] 5 6 const headAndTail = (head, ...tail) => [head, tail]; 7 8 headAndTail(1, 2, 3, 4, 5) 9 // [1,[2,3,4,5]]
(1)函數體內的this
對象,就是定義時所在的對象,而不是使用時所在的對象。
(2)不能夠看成構造函數,也就是說,不可使用new
命令,不然會拋出一個錯誤。
(3)不可使用arguments
對象,該對象在函數體內不存在。若是要用,能夠用 rest 參數代替。
(4)不可使用yield
命令,所以箭頭函數不能用做 Generator 函數。
上面四點中,第一點尤爲值得注意。this
對象的指向是可變的,可是在箭頭函數中,它是固定的。
1 function foo() { 2 setTimeout(() => { 3 console.log('id:', this.id); 4 }, 100); 5 } 6 7 var id = 21; 8 9 foo.call({ id: 42 }); 10 // id: 42
上面代碼中,setTimeout
的參數是一個箭頭函數,這個箭頭函數的定義生效是在foo
函數生成時,而它的真正執行要等到 100 毫秒後。若是是普通函數,執行時this
應該指向全局對象window
,這時應該輸出21
。可是,箭頭函數致使this
老是指向函數定義生效時所在的對象(本例是{id: 42}
),因此輸出的是42
。
箭頭函數可讓setTimeout
裏面的this
,綁定定義時所在的做用域,而不是指向運行時所在的做用域。下面是另外一個例子。
1 function Timer() { 2 this.s1 = 0; 3 this.s2 = 0; 4 // 箭頭函數 5 setInterval(() => this.s1++, 1000); 6 // 普通函數 7 setInterval(function () { 8 this.s2++; 9 }, 1000); 10 } 11 12 var timer = new Timer(); 13 14 setTimeout(() => console.log('s1: ', timer.s1), 3100); 15 setTimeout(() => console.log('s2: ', timer.s2), 3100); 16 // s1: 3 17 // s2: 0
上面代碼中,Timer
函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this
綁定定義時所在的做用域(即Timer
函數),後者的this
指向運行時所在的做用域(即全局對象)。因此,3100 毫秒以後,timer.s1
被更新了 3 次,而timer.s2
一次都沒更新。
箭頭函數可讓this
指向固定化,這種特性頗有利於封裝回調函數。下面是一個例子,DOM 事件的回調函數封裝在一個對象裏面。
1 var handler = { 2 id: '123456', 3 4 init: function() { 5 document.addEventListener('click', 6 event => this.doSomething(event.type), false); 7 }, 8 9 doSomething: function(type) { 10 console.log('Handling ' + type + ' for ' + this.id); 11 } 12 };
上面代碼的init
方法中,使用了箭頭函數,這致使這個箭頭函數裏面的this
,老是指向handler
對象。不然,回調函數運行時,this.doSomething
這一行會報錯,由於此時this
指向document
對象。
this
指向的固定化,並非由於箭頭函數內部有綁定this
的機制,實際緣由是箭頭函數根本沒有本身的this
,致使內部的this
就是外層代碼塊的this
。正是由於它沒有this
,因此也就不能用做構造函數。
1 // ES6 2 function foo() { 3 setTimeout(() => { 4 console.log('id:', this.id); 5 }, 100); 6 } 7 8 // ES5 9 function foo() { 10 var _this = this; 11 12 setTimeout(function () { 13 console.log('id:', _this.id); 14 }, 100); 15 }
上面代碼中,轉換後的 ES5 版本清楚地說明了,箭頭函數裏面根本沒有本身的this
,而是引用外層的this
請問下面的代碼之中有幾個this
?
1 function foo() { 2 return () => { 3 return () => { 4 return () => { 5 console.log('id:', this.id); 6 }; 7 }; 8 }; 9 } 10 11 var f = foo.call({id: 1}); 12 13 var t1 = f.call({id: 2})()(); // id: 1 14 var t2 = f().call({id: 3})(); // id: 1 15 var t3 = f()().call({id: 4}); // id: 1
上面代碼之中,只有一個this
,就是函數foo
的this
,因此t1
、t2
、t3
都輸出一樣的結果。由於全部的內層函數都是箭頭函數,都沒有本身的this
,它們的this
其實都是最外層foo
函數的this
。
除了this
,如下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:arguments
、super
、new.target
。
1 function foo() { 2 setTimeout(() => { 3 console.log('args:', arguments); 4 }, 100); 5 } 6 7 foo(2, 4, 6, 8) 8 // args: [2, 4, 6, 8]
上面代碼中,箭頭函數內部的變量arguments
,實際上是函數foo
的arguments
變量。
另外,因爲箭頭函數沒有本身的this
,因此固然也就不能用call()
、apply()
、bind()
這些方法去改變this
的指向。
1 (function() { 2 return [ 3 (() => this.x).bind({ x: 'inner' })() 4 ]; 5 }).call({ x: 'outer' }); 6 // ['outer']
上面代碼中,箭頭函數沒有本身的this
,因此bind
方法無效,內部的this
指向外部的this
。
長期以來,JavaScript 語言的this
對象一直是一個使人頭痛的問題,在對象方法中使用this
,必須很是當心。箭頭函數」綁定」this
,很大程度上解決了這個困擾。
箭頭函數內部,還能夠再使用箭頭函數。下面是一個 ES5 語法的多重嵌套函數。
1 function insert(value) { 2 return {into: function (array) { 3 return {after: function (afterValue) { 4 array.splice(array.indexOf(afterValue) + 1, 0, value); 5 return array; 6 }}; 7 }}; 8 } 9 10 insert(2).into([1, 3]).after(1); //[1, 2, 3]
上面這個函數,可使用箭頭函數改寫。
1 let insert = (value) => ({into: (array) => ({after: (afterValue) => { 2 array.splice(array.indexOf(afterValue) + 1, 0, value); 3 return array; 4 }})}); 5 6 insert(2).into([1, 3]).after(1); //[1, 2, 3]
箭頭函數能夠綁定this
對象,大大減小了顯式綁定this
對象的寫法(call
、apply
、bind
)。可是,箭頭函數並不適用於全部場合,因此如今有一個提案,提出了「函數綁定」(function bind)運算符,用來取代call
、apply
、bind
調用。
函數綁定運算符是並排的兩個冒號(::
),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,做爲上下文環境(即this
對象),綁定到右邊的函數上面。
1 foo::bar; 2 // 等同於 3 bar.bind(foo); 4 5 foo::bar(...arguments); 6 // 等同於 7 bar.apply(foo, arguments); 8 9 const hasOwnProperty = Object.prototype.hasOwnProperty; 10 function hasOwn(obj, key) { 11 return obj::hasOwnProperty(key); 12 }
若是雙冒號左邊爲空,右邊是一個對象的方法,則等於將該方法綁定在該對象上面。
1 var method = obj::obj.foo; 2 // 等同於 3 var method = ::obj.foo; 4 5 let log = ::console.log; 6 // 等同於 7 var log = console.log.bind(console);
若是雙冒號運算符的運算結果,仍是一個對象,就能夠採用鏈式寫法。
1 import { map, takeWhile, forEach } from "iterlib"; 2 3 getPlayers() 4 ::map(x => x.character()) 5 ::takeWhile(x => x.strength > 100) 6 ::forEach(x => console.log(x));
尾調用(Tail Call)是函數式編程的一個重要概念,自己很是簡單,一句話就能說清楚,就是指某個函數的最後一步是調用另外一個函數。
1 function f(x){ 2 return g(x); 3 }
上面代碼中,函數f
的最後一步是調用函數g
,這就叫尾調用。
如下三種狀況,都不屬於尾調用
1 // 狀況一 2 function f(x){ 3 let y = g(x); 4 return y; 5 } 6 7 // 狀況二 8 function f(x){ 9 return g(x) + 1; 10 } 11 12 // 狀況三 13 function f(x){ 14 g(x); 15 }
上面代碼中,狀況一是調用函數g
以後,還有賦值操做,因此不屬於尾調用,即便語義徹底同樣。狀況二也屬於調用後還有操做,即便寫在一行內。狀況三等同於下面的代碼。
1 function f(x){ 2 g(x); 3 return undefined; 4 }
尾調用不必定出如今函數尾部,只要是最後一步操做便可。
1 function f(x) { 2 if (x > 0) { 3 return m(x) 4 } 5 return n(x); 6 }
上面代碼中,函數m
和n
都屬於尾調用,由於它們都是函數f
的最後一步操做。
尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就能夠了。
1 function f() { 2 let m = 1; 3 let n = 2; 4 return g(m + n); 5 } 6 f(); 7 8 // 等同於 9 function f() { 10 return g(3); 11 } 12 f(); 13 14 // 等同於 15 g(3);
上面代碼中,若是函數g
不是尾調用,函數f
就須要保存內部變量m
和n
的值、g
的調用位置等信息。但因爲調用g
以後,函數f
就結束了,因此執行到最後一步,徹底能夠刪除f(x)
的調用幀,只保留g(3)
的調用幀。
這就叫作「尾調用優化」(Tail call optimization),即只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有一項,這將大大節省內存。這就是「尾調用優化」的意義。
函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。
遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生「棧溢出」錯誤。
1 function factorial(n) { 2 if (n === 1) return 1; 3 return n * factorial(n - 1); 4 } 5 6 factorial(5) // 120
上面代碼是一個階乘函數,計算n
的階乘,最多須要保存n
個調用記錄,複雜度 O(n)
若是改寫成尾遞歸,只保留一個調用記錄,複雜度 O(1) 。
1 function factorial(n, total) { 2 if (n === 1) return total; 3 return factorial(n - 1, n * total); 4 } 5 6 factorial(5, 1) // 120
尾遞歸的實現,每每須要改寫遞歸函數,確保最後一步只調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。好比上面的例子,階乘函數 factorial 須要用到一箇中間變量total
,那就把這個中間變量改寫成函數的參數。這樣作的缺點就是不太直觀,第一眼很難看出來,爲何計算5
的階乘,須要傳入兩個參數5
和1
?
1 function tailFactorial(n, total) { 2 if (n === 1) return total; 3 return tailFactorial(n - 1, n * total); 4 } 5 6 function factorial(n) { 7 return tailFactorial(n, 1); 8 } 9 10 factorial(5) // 120