ES6之路第五篇:函數的擴展

屬性的簡潔表示法

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屬性

 函數的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,就是函數foothis,因此t1t2t3都輸出一樣的結果。由於全部的內層函數都是箭頭函數,都沒有本身的this,它們的this其實都是最外層foo函數的this

除了this,如下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:argumentssupernew.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,實際上是函數fooarguments變量。

另外,因爲箭頭函數沒有本身的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對象的寫法(callapplybind)。可是,箭頭函數並不適用於全部場合,因此如今有一個提案,提出了「函數綁定」(function bind)運算符,用來取代callapplybind調用。

函數綁定運算符是並排的兩個冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,做爲上下文環境(即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));

尾調用優化

1.什麼是尾調用

尾調用(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 }

上面代碼中,函數mn都屬於尾調用,由於它們都是函數f的最後一步操做。

2.尾調用優化

尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就能夠了。

 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就須要保存內部變量mn的值、g的調用位置等信息。但因爲調用g以後,函數f就結束了,因此執行到最後一步,徹底能夠刪除f(x)的調用幀,只保留g(3)的調用幀。

這就叫作「尾調用優化」(Tail call optimization),即只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有一項,這將大大節省內存。這就是「尾調用優化」的意義。

3.尾遞歸

函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。

遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(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

4.遞歸函數的改寫

尾遞歸的實現,每每須要改寫遞歸函數,確保最後一步只調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。好比上面的例子,階乘函數 factorial 須要用到一箇中間變量total,那就把這個中間變量改寫成函數的參數。這樣作的缺點就是不太直觀,第一眼很難看出來,爲何計算5的階乘,須要傳入兩個參數51

 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
相關文章
相關標籤/搜索