<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 第七章:元編程 元編程是針對程序本身的行為進行操作的編程。換句話說,它是為你程序的編程而進行的編程。是的,很拗口,對吧? 例如,如果你為了調查對象`a`和另一個對象`b`之間的關系 —— 它們是被`[[Prototype]]`鏈接的嗎? —— 而使用`a.isPrototypeOf(b)`,這通常稱為自省,就是一種形式的元編程。宏(JS中還沒有) —— 代碼在編譯時修改自己 —— 是元編程的另一個明顯的例子。使用`for..in`循環枚舉一個對象的鍵,或者檢查一個對象是否是一個“類構造器”的?*實例*,是另一些常見的元編程任務。 元編程關注以下的一點或幾點:代碼檢視自己,代碼修改自己,或者代碼修改默認的語言行為而使其他代碼受影響。 元編程的目標是利用語言自身的內在能力使你其他部分的代碼更具描述性,表現力,和/或靈活性。由于元編程的?*元*?的性質,要給它一個更精確的定義有些困難。理解元編程的最佳方法是通過代碼來觀察它。 ES6在JS已經擁有的東西上,增加了幾種新的元編程形式/特性。 ## 函數名 有一些情況,你的代碼想要檢視自己并詢問某個函數的名稱是什么。如果你詢問一個函數的名稱,答案會有些令人詫異地模糊。考慮如下代碼: ```source-js function daz() { // .. } var obj = { foo: function() { // .. }, bar: function baz() { // .. }, bam: daz, zim() { // .. } }; ``` 在這前一個代碼段中,“`obj.foo()`的名字是什么?”有些微妙。是`"foo"`,`""`,還是`undefined`?那么`obj.bar()`呢 —— 是`"bar"`還是`"baz"`?`obj.bam()`稱為`"bam"`還是`"daz"`?`obj.zim()`呢? 另外,作為回調被傳遞的函數呢?就像: ```source-js function foo(cb) { // 這里的 `cb()` 的名字是什么? } foo( function(){ // 我是匿名的! } ); ``` 在程序中函數可以被好幾種方法所表達,而函數的“名字”應當是什么并不總是那么清晰和明確。 更重要的是,我們需要區別函數的“名字”是指它的`name`屬性 —— 是的,函數有一個叫做`name`的屬性 —— 還是指它詞法綁定的名稱,比如在`function bar() { .. }`中的`bar`。 詞法綁定名稱是你將在遞歸之類的東西中所使用的: ```source-js function foo(i) { if (i < 10) return foo( i * 2 ); return i; } ``` `name`屬性是你為了元編程而使用的,所以它才是我們在這里的討論中所關注的。 產生這種用困惑是因為,在默認情況下一個函數的詞法名稱(如果有的話)也會被設置為它的`name`屬性。實際上,ES5(和以前的)語言規范中并沒有官方要求這種行為。`name`屬性的設置是一種非標準,但依然相當可靠的行為。在ES6中,它已經被標準化。 提示:?如果一個函數的`name`被賦值,它通常是在開發者工具的棧軌跡中使用的名稱。 ### 推斷 但如果函數沒有詞法名稱,`name`屬性會怎么樣呢? 現在在ES6中,有一個推斷規則可以判定一個合理的`name`屬性值來賦予一個函數,即使它沒有詞法名稱可用。 考慮如下代碼: ```source-js var abc = function() { // .. }; abc.name; // "abc" ``` 如果我們給了這個函數一個詞法名稱,比如`abc = function def() { .. }`,那么`name`屬性將理所當然地是`"def"`。但是由于缺少詞法名稱,直觀上名稱`"abc"`看起來很合適。 這里是在ES6中將會(或不會)進行名稱推斷的其他形式: ```source-js (function(){ .. }); // name: (function*(){ .. }); // name: window.foo = function(){ .. }; // name: class Awesome { constructor() { .. } // name: Awesome funny() { .. } // name: funny } var c = class Awesome { .. }; // name: Awesome var o = { foo() { .. }, // name: foo *bar() { .. }, // name: bar baz: () => { .. }, // name: baz bam: function(){ .. }, // name: bam get qux() { .. }, // name: get qux set fuz() { .. }, // name: set fuz ["b" + "iz"]: function(){ .. }, // name: biz [Symbol( "buz" )]: function(){ .. } // name: [buz] }; var x = o.foo.bind( o ); // name: bound foo (function(){ .. }).bind( o ); // name: bound export default function() { .. } // name: default var y = new Function(); // name: anonymous var GeneratorFunction = function*(){}.__proto__.constructor; var z = new GeneratorFunction(); // name: anonymous ``` `name`屬性默認是不可寫的,但它是可配置的,這意味著如果有需要,你可以使用`Object.defineProperty(..)`來手動改變它。 ## 元屬性 在第三章的“`new.target`”一節中,我們引入了一個ES6的新概念:元屬性。正如這個名稱所暗示的,元屬性意在以一種屬性訪問的形式提供特殊的元信息,而這在以前是不可能的。 在`new.target`的情況下,關鍵字`new`作為一個屬性訪問的上下文環境。顯然`new`本身不是一個對象,這使得這種能力很特殊。然而,當`new.target`被用于一個構造器調用(一個使用`new`調用的函數/方法)內部時,`new`變成了一個虛擬上下文環境,如此`new.target`就可以指代這個`new`調用的目標構造器。 這是一個元編程操作的典型例子,因為它的意圖是從一個構造器調用內部判定原來的`new`的目標是什么,這一般是為了自省(檢查類型/結構)或者靜態屬性訪問。 舉例來說,你可能想根據一個構造器是被直接調用,還是通過一個子類進行調用,來使它有不同的行為: ```source-js class Parent { constructor() { if (new.target === Parent) { console.log( "Parent instantiated" ); } else { console.log( "A child instantiated" ); } } } class Child extends Parent {} var a = new Parent(); // Parent instantiated var b = new Child(); // A child instantiated ``` 這里有一個微妙的地方,在`Parent`類定義內部的`constructor()`實際上被給予了這個類的詞法名稱(`Parent`),即便語法暗示著這個類是一個與構造器分離的不同實體。 警告:?與所有的元編程技術一樣,要小心不要創建太過聰明的代碼,而使未來的你或其他維護你代碼的人很難理解。小心使用這些技巧。 ## 通用 Symbol 在第二章中的“Symbol”一節中,我們講解了新的ES6基本類型`symbol`。除了你可以在你自己的程序中定義的symbol以外,JS預定義了幾種內建symbol,被稱為?*通用(Well Known) Symbols*(WKS)。 定義這些symbol值主要是為了向你的JS程序暴露特殊的元屬性來給你更多JS行為的控制權。 我們將簡要介紹每一個symbol并討論它們的目的。 ### `Symbol.iterator` 在第二和第三章中,我們介紹并使用了`@@iterator`symbol,它被自動地用于`...`擴散和`for..of`循環。我們還在第五章中看到了在新的ES6集合中定義的`@@iterator`。 `Symbol.iterator`表示在任意一個對象上的特殊位置(屬性),語言機制自動地在這里尋找一個方法,這個方法將構建一個用于消費對象值的迭代器對象。許多對象都帶有一個默認的`Symbol.iterator`。 然而,我們可以通過設置`Symbol.iterator`屬性來為任意對象定義我們自己的迭代器邏輯,即便它是覆蓋默認迭代器的。這里的元編程觀點是,我們在定義JS的其他部分(明確地說,是操作符和循環結構)在處理我們所定義的對象值時所使用的行為。 考慮如下代碼: ```source-js var arr = [4,5,6,7,8,9]; for (var v of arr) { console.log( v ); } // 4 5 6 7 8 9 // 定義一個僅在奇數索引處產生值的迭代器 arr[Symbol.iterator] = function*() { var idx = 1; do { yield this[idx]; } while ((idx += 2) < this.length); }; for (var v of arr) { console.log( v ); } // 5 7 9 ``` ### `Symbol.toStringTag`?和`Symbol.hasInstance` 最常見的元編程任務之一,就是在一個值上進行自省來找出它是什么?*種類*?的,者經常用來決定它們上面適于實施什么操作。對于對象,最常見的兩個自省技術是`toString()`和`instanceof`。 考慮如下代碼: ```source-js function Foo() {} var a = new Foo(); a.toString(); // [object Object] a instanceof Foo; // true ``` 在ES6中,你可以控制這些操作的行為: ```source-js function Foo(greeting) { this.greeting = greeting; } Foo.prototype[Symbol.toStringTag] = "Foo"; Object.defineProperty( Foo, Symbol.hasInstance, { value: function(inst) { return inst.greeting == "hello"; } } ); var a = new Foo( "hello" ), b = new Foo( "world" ); b[Symbol.toStringTag] = "cool"; a.toString(); // [object Foo] String( b ); // [object cool] a instanceof Foo; // true b instanceof Foo; // false ``` 在原型(或實例本身)上的`@@toStringTag`symbol指定一個用于`[object ___]`字符串化的字符串值。 `@@hasInstance`symbol是一個在構造器函數上的方法,它接收一個實例對象值并讓你通過放回`true`或`false`來決定這個值是否應當被認為是一個實例。 注意:?要在一個函數上設置`@@hasInstance`,你必須使用`Object.defineProperty(..)`,因為在`Function.prototype`上默認的那一個是`writable: false`。更多信息參見本系列的?*this與對象原型*。 ### `Symbol.species` 在第三章的“類”中,我們介紹了`@@species`symbol,它控制一個類內建的生成新實例的方法使用哪一個構造器。 最常見的例子是,在子類化`Array`并且想要定義`slice(..)`之類被繼承的方法應當使用哪一個構造器時。默認地,在一個`Array`的子類實例上調用的`slice(..)`將產生這個子類的實例,坦白地說這正是你經常希望的。 但是,你可以通過覆蓋一個類的默認`@@species`定義來進行元編程: ```source-js class Cool { // 將 `@@species` 倒推至被衍生的構造器 static get [Symbol.species]() { return this; } again() { return new this.constructor[Symbol.species](); } } class Fun extends Cool {} class Awesome extends Cool { // 將 `@@species` 強制為父類構造器 static get [Symbol.species]() { return Cool; } } var a = new Fun(), b = new Awesome(), c = a.again(), d = b.again(); c instanceof Fun; // true d instanceof Awesome; // false d instanceof Cool; // true ``` 就像在前面的代碼段中的`Cool`的定義展示的那樣,在內建的原生構造器上的`Symbol.species`設定默認為`return this`。它在用戶自己的類上沒有默認值,但也像展示的那樣,這種行為很容易模擬。 如果你需要定義生成新實例的方法,使用`new this.constructor[Symbol.species](..)`的元編程模式,而不要用手寫的`new this.constructor(..)`或者`new XYZ(..)`。如此衍生的類就能夠自定義`Symbol.species`來控制哪一個構造器來制造這些實例。 ### `Symbol.toPrimitive` 在本系列的?*類型與文法*?一書中,我們討論了`ToPrimitive`抽象強制轉換操作,它在對象為了某些操作(例如`==`比較或者`+`加法)而必須被強制轉換為一個基本類型值時被使用。在ES6以前,沒有辦法控制這個行為。 在ES6中,在任意對象值上作為屬性的`@@toPrimitive`symbol都可以通過指定一個方法來自定義這個`ToPrimitive`強制轉換。 考慮如下代碼: ```source-js var arr = [1,2,3,4,5]; arr + 10; // 1,2,3,4,510 arr[Symbol.toPrimitive] = function(hint) { if (hint == "default" || hint == "number") { // 所有數字的和 return this.reduce( function(acc,curr){ return acc + curr; }, 0 ); } }; arr + 10; // 25 ``` `Symbol.toPrimitive`方法將根據調用`ToPrimitive`的操作期望何種類型,而被提供一個值為`"string"`,`"number"`,或`"default"`(這應當被解釋為`"number"`)的?*提示(hint)*。在前一個代碼段中,`+`加法操作沒有提示(`"default"`將被傳遞)。一個`*`乘法操作將提示`"number"`,而一個`String(arr)`將提示`"string"`。 警告:?`==`操作符將在一個對象上不使用任何提來示調用`ToPrimitive`操作 —— 如果存在`@@toPrimitive`方法的話,將使用`"default"`被調用 —— 如果另一個被比較的值不是一個對象。但是,如果兩個被比較的值都是對象,`==`的行為與`===`是完全相同的,也就是引用本身將被直接比較。這種情況下,`@@toPrimitive`根本不會被調用。關于強制轉換和抽象操作的更多信息,參見本系列的?*類型與文法*。 ### 正則表達式 Symbols 對于正則表達式對象,有四種通用 symbols 可以被覆蓋,它們控制著這些正則表達式在四個相應的同名`String.prototype`函數中如何被使用: * `@@match`:一個正則表達式的`Symbol.match`值是使用被給定的正則表達式來匹配一個字符串值的全部或部分的方法。如果你為`String.prototype.match(..)`傳遞一個正則表達式做范例匹配,它就會被使用。 匹配的默認算法寫在ES6語言規范的第21.2.5.6部分([https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@match)。你可以覆蓋這個默認算法并提供額外的正則表達式特性,比如后顧斷言。](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@match) `Symbol.match`還被用于`isRegExp`抽象操作(參見第六章的“字符串檢測函數”中的注意部分)來判定一個對象是否意在被用作正則表達式。為了使一個這樣的對象不被看作是正則表達式,可以將`Symbol.match`的值設置為`false`(或falsy的東西)強制這個檢查失敗。 * `@@replace`:一個正則表達式的`Symbol.replace`值是被`String.prototype.replace(..)`使用的方法,來替換一個字符串里面出現的一個或所有字符序列,這些字符序列匹配給出的正則表達式范例。 替換的默認算法寫在ES6語言規范的第21.2.5.8部分([https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@replace)。](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@replace) 一個覆蓋默認算法的很酷的用法是提供額外的`replacer`可選參數值,比如通過用連續的替換值消費可迭代對象來支持`"abaca".replace(/a/g,[1,2,3])`產生`"1b2c3"`。 * `@@search`:一個正則表達式的`Symbol.search`值是被`String.prototype.search(..)`使用的方法,來在一個字符串中檢索一個匹配給定正則表達式的子字符串。 檢索的默認算法寫在ES6語言規范的第21.2.5.9部分([https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@search)。](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@search) * `@@split`:一個正則表達式的`Symbol.split`值是被`String.prototype.split(..)`使用的方法,來將一個字符串在分隔符匹配給定正則表達式的位置分割為子字符串。 分割的默認算法寫在ES6語言規范的第21.2.5.11部分([https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@split)。](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@split) 覆蓋內建的正則表達式算法不是為心臟脆弱的人準備的!JS帶有高度優化的正則表達式引擎,所以你自己的用戶代碼將很可能慢得多。這種類型的元編程很精巧和強大,但是應當僅用于確實必要或有好處的情況下。 ### `Symbol.isConcatSpreadable` `@@isConcatSpreadable`symbol可以作為一個布爾屬性(`Symbol.isConcatSpreadable`)在任意對象上(比如一個數組或其他的可迭代對象)定義,來指示當它被傳遞給一個數組`concat(..)`時是否應當被?*擴散*。 考慮如下代碼: ```source-js var a = [1,2,3], b = [4,5,6]; b[Symbol.isConcatSpreadable] = false; [].concat( a, b ); // [1,2,3,[4,5,6]] ``` ### `Symbol.unscopables` `@@unscopables`symbol可以作為一個對象屬性(`Symbol.unscopables`)在任意對象上定義,來指示在一個`with`語句中哪一個屬性可以和不可以作為此法變量被暴露。 考慮如下代碼: ```source-js var o = { a:1, b:2, c:3 }, a = 10, b = 20, c = 30; o[Symbol.unscopables] = { a: false, b: true, c: false }; with (o) { console.log( a, b, c ); // 1 20 3 } ``` 一個在`@@unscopables`對象中的`true`指示這個屬性應當是?*非作用域(unscopable)*?的,因此會從此法作用域變量中被過濾掉。`false`意味著它可以被包含在此法作用域變量中。 警告:?`with`語句在`strict`模式下是完全禁用的,而且因此應當被認為是在語言中被廢棄的。不要使用它。更多信息參見本系列的?*作用域與閉包*。因為應當避免`with`,所以這個`@@unscopables`symbol也是無意義的。 ## 代理 在ES6中被加入的最明顯的元編程特性之一就是`proxy`特性。 一個代理是一種由你創建的特殊的對象,它“包”著另一個普通的對象 —— 或者說擋在這個普通對象的前面。你可以在代理對象上注冊特殊的處理器(也叫?*機關(traps)*),當對這個代理實施各種操作時被調用。這些處理器除了將操作?*傳送*?到原本的目標/被包裝的對象上之外,還有機會運行額外的邏輯。 一個這樣的?*機關*?處理器的例子是,你可以在一個代理上定義一個攔截`[[Get]]`操作的`get`?—— 它在當你試圖訪問一個對象上的屬性時運行。考慮如下代碼: ```source-js var obj = { a: 1 }, handlers = { get(target,key,context) { // 注意:target === obj, // context === pobj console.log( "accessing: ", key ); return Reflect.get( target, key, context ); } }, pobj = new Proxy( obj, handlers ); obj.a; // 1 pobj.a; // accessing: a // 1 ``` 我們將一個`get(..)`處理器作為?*處理器*?對象的命名方法聲明(`Proxy(..)`的第二個參數值),它接收一個指向?*目標*?對象的引用(`obj`),屬性的?*鍵*?名稱(`"a"`),和`self`/接受者/代理本身(`pobj`)。 在追蹤語句`console.log(..)`之后,我們通過`Reflect.get(..)`將操作“轉送”到`obj`。我們將在下一節詳細講解`Reflect`API,但要注意的是每個可用的代理機關都有一個相應的同名`Reflect`函數。 這些映射是故意對稱的。每個代理處理器在各自的元編程任務實施時進行攔截,而每個`Reflect`工具將各自的元編程任務在一個對象上實施。每個代理處理器都有一個自動調用相應`Reflect`工具的默認定義。幾乎可以肯定你將總是一前一后地使用`Proxy`和`Reflect`。 這里的列表是你可以在一個代理上為一個?*目標*?對象/函數定義的處理器,以及它們如何/何時被觸發: * `get(..)`:通過`[[Get]]`,在代理上訪問一個屬性(`Reflect.get(..)`,`.`屬性操作符或`[ .. ]`屬性操作符) * `set(..)`:通過`[[Set]]`,在代理對象上設置一個屬性(`Reflect.set(..)`,`=`賦值操作符,或者解構賦值 —— 如果目標是一個對象屬性的話) * `deleteProperty(..)`:通過`[[Delete]]`,在代理對象上刪除一個屬性 (`Reflect.deleteProperty(..)`或`delete`) * `apply(..)`(如果?*目標*?是一個函數):通過`[[Call]]`,代理作為一個普通函數/方法被調用(`Reflect.apply(..)`,`call(..)`,`apply(..)`,或者`(..)`調用操作符) * `construct(..)`(如果?*目標*?是一個構造函數):通過`[[Construct]]`代理作為一個構造器函數被調用(`Reflect.construct(..)`或`new`) * `getOwnPropertyDescriptor(..)`:通過`[[GetOwnProperty]]`,從代理取得一個屬性的描述符(`Object.getOwnPropertyDescriptor(..)`或`Reflect.getOwnPropertyDescriptor(..)`) * `defineProperty(..)`:通過`[[DefineOwnProperty]]`,在代理上設置一個屬性描述符(`Object.defineProperty(..)`或`Reflect.defineProperty(..)`) * `getPrototypeOf(..)`:通過`[[GetPrototypeOf]]`,取得代理的`[[Prototype]]`(`Object.getPrototypeOf(..)`,`Reflect.getPrototypeOf(..)`,`__proto__`,?`Object#isPrototypeOf(..)`,或`instanceof`) * `setPrototypeOf(..)`:通過`[[SetPrototypeOf]]`,設置代理的`[[Prototype]]`(`Object.setPrototypeOf(..)`,`Reflect.setPrototypeOf(..)`,或`__proto__`) * `preventExtensions(..)`:通過`[[PreventExtensions]]`使代理成為不可擴展的(`Object.preventExtensions(..)`或`Reflect.preventExtensions(..)`) * `isExtensible(..)`:通過`[[IsExtensible]]`,檢測代理的可擴展性(`Object.isExtensible(..)`或`Reflect.isExtensible(..)`) * `ownKeys(..)`:通過`[[OwnPropertyKeys]]`,取得一組代理的直屬屬性和/或直屬symbol屬性(`Object.keys(..)`,`Object.getOwnPropertyNames(..)`,`Object.getOwnSymbolProperties(..)`,`Reflect.ownKeys(..)`,或`JSON.stringify(..)`) * `enumerate(..)`:通過`[[Enumerate]]`,為代理的可枚舉直屬屬性及“繼承”屬性請求一個迭代器(`Reflect.enumerate(..)`或`for..in`) * `has(..)`:通過`[[HasProperty]]`,檢測代理是否擁有一個直屬屬性或“繼承”屬性(`Reflect.has(..)`,`Object#hasOwnProperty(..)`,或`"prop" in obj`) 提示:?關于每個這些元編程任務的更多信息,參見本章稍后的“`Reflect`?API”一節。 關于將會觸發各種機關的動作,除了在前面列表中記載的以外,一些機關還會由另一個機關的默認動作間接地觸發。舉例來說: ```source-js var handlers = { getOwnPropertyDescriptor(target,prop) { console.log( "getOwnPropertyDescriptor" ); return Object.getOwnPropertyDescriptor( target, prop ); }, defineProperty(target,prop,desc){ console.log( "defineProperty" ); return Object.defineProperty( target, prop, desc ); } }, proxy = new Proxy( {}, handlers ); proxy.a = 2; // getOwnPropertyDescriptor // defineProperty ``` 在設置一個屬性值時(不管是新添加還是更新),`getOwnPropertyDescriptor(..)`和`defineProperty(..)`處理器被默認的`set(..)`處理器觸發。如果你還定義了你自己的`set(..)`處理器,你或許對`context`(不是`target`!)進行了將會觸發這些代理機關的相應調用。 ### 代理的限制 這些元編程處理器攔截了你可以對一個對象進行的范圍很廣泛的一組基礎操作。但是,有一些操作不能(至少是還不能)被用于攔截。 例如,從`pobj`代理到`obj`目標,這些操作全都沒有被攔截和轉送: ```source-js var obj = { a:1, b:2 }, handlers = { .. }, pobj = new Proxy( obj, handlers ); typeof obj; String( obj ); obj + ""; obj == pobj; obj === pobj ``` 也許在未來,更多這些語言中的底層基礎操作都將是可攔截的,那將給我們更多力量來從JavaScript自身擴展它。 警告:?對于代理處理器的使用來說存在某些?*不變量*?—— 它們的行為不能被覆蓋。例如,`isExtensible(..)`處理器的結果總是被強制轉換為一個`boolean`。這些不變量限制了一些你可以使用代理來自定義行為的能力,但是它們這樣做只是為了防止你創建奇怪和不尋常(或不合邏輯)的行為。這些不變量的條件十分復雜,所以我們就不再這里全面闡述了,但是這篇博文([http://www.2ality.com/2014/12/es6-proxies.html#invariants)很好地講解了它們。](http://www.2ality.com/2014/12/es6-proxies.html#invariants) ### 可撤銷的代理 一個一般的代理總是包裝著目標對象,而且在創建之后就不能修改了 —— 只要保持著一個指向這個代理的引用,代理的機制就將維持下去。但是,可能會有一些情況你想要創建一個這樣的代理:在你想要停止它作為代理時可以被停用。解決方案就是創建一個?*可撤銷代理*: ```source-js var obj = { a: 1 }, handlers = { get(target,key,context) { // 注意:target === obj, // context === pobj console.log( "accessing: ", key ); return target[key]; } }, { proxy: pobj, revoke: prevoke } = Proxy.revocable( obj, handlers ); pobj.a; // accessing: a // 1 // 稍后: prevoke(); pobj.a; // TypeError ``` 一個可撤銷代理是由`Proxy.revocable(..)`創建的,它是一個普通的函數,不是一個像`Proxy(..)`那樣的構造器。此外,它接收同樣的兩個參數值:*目標*?和?*處理器*。 與`new Proxy(..)`不同的是,`Proxy.revocable(..)`的返回值不是代理本身。取而代之的是,它返回一個帶有?*proxy*?和?*revoke*兩個屬性的對象 —— 我們使用了對象解構(參見第二章的“解構”)來將這些屬性分別賦值給變量`pobj`和`prevoke`。 一旦可撤銷代理被撤銷,任何訪問它的企圖(觸發它的任何機關)都將拋出`TypeError`。 一個使用可撤銷代理的例子可能是,將一個代理交給另一個存在于你應用中、并管理你模型中的數據的團體,而不是給它們一個指向正式模型對象本身的引用。如果你的模型對象改變了或者被替換掉了,你希望廢除這個你交出去的代理,以便于其他的團體能夠(通過錯誤!)知道要請求一個更新過的模型引用。 ### 使用代理 這些代理處理器帶來的元編程的好處應當是顯而易見的。我們可以全面地攔截(而因此覆蓋)對象的行為,這意味著我們可以用一些非常強大的方式將對象行為擴展至JS核心之外。我們將看幾個模式的例子來探索這些可能性。 #### 代理前置,代理后置 正如我們早先提到過的,你通常將一個代理考慮為一個目標對象的“包裝”。在這種意義上,代理就變成了代碼接口所針對的主要對象,而實際的目標對象則保持被隱藏/被保護的狀態。 你可能這么做是因為你希望將對象傳遞到某個你不能完全“信任”的地方去,如此你需要在它的訪問權上強制實施一些特殊的規則,而不是傳遞這個對象本身。 考慮如下代碼: ```source-js var messages = [], handlers = { get(target,key) { // 是字符串值嗎? if (typeof target[key] == "string") { // 過濾掉標點符號 return target[key] .replace( /[^\w]/g, "" ); } // 讓其余的東西通過 return target[key]; }, set(target,key,val) { // 僅設置唯一的小寫字符串 if (typeof val == "string") { val = val.toLowerCase(); if (target.indexOf( val ) == -1) { target.push(val); } } return true; } }, messages_proxy = new Proxy( messages, handlers ); // 在別處: messages_proxy.push( "heLLo...", 42, "wOrlD!!", "WoRld!!" ); messages_proxy.forEach( function(val){ console.log(val); } ); // hello world messages.forEach( function(val){ console.log(val); } ); // hello... world!! ``` 我稱此為?*代理前置*?設計,因為我們首先(主要、完全地)與代理進行互動。 我們在與`messages_proxy`的互動上強制實施了一些特殊規則,這些規則不會強制實施在`messages`本身上。我們僅在值是一個不重復的字符串時才將它添加為元素;我們還將這個值變為小寫。當從`messages_proxy`取得值時,我們過濾掉字符串中所有的標點符號。 另一種方式是,我們可以完全反轉這個模式,讓目標與代理交互而不是讓代理與目標交互。這樣,代碼其實只與主對象交互。達成這種后備方案的最簡單的方法是,讓代理對象存在于主對象的`[[Prototype]]`鏈中。 考慮如下代碼: ```source-js var handlers = { get(target,key,context) { return function() { context.speak(key + "!"); }; } }, catchall = new Proxy( {}, handlers ), greeter = { speak(who = "someone") { console.log( "hello", who ); } }; // 讓 `catchall` 成為 `greeter` 的后備方法 Object.setPrototypeOf( greeter, catchall ); greeter.speak(); // hello someone greeter.speak( "world" ); // hello world greeter.everyone(); // hello everyone! ``` 我們直接與`greeter`而非`catchall`進行交互。當我們調用`speak(..)`時,它在`greeter`上被找到并直接使用。但當我們試圖訪問`everyone()`這樣的方法時,這個函數并不存在于`greeter`。 默認的對象屬性行為是向上檢查`[[Prototype]]`鏈(參見本系列的?*this與對象原型*),所以`catchall`被詢問有沒有一個`everyone`屬性。然后代理的`get()`處理器被調用并返回一個函數,這個函數使用被訪問的屬性名(`"everyone"`)調用`speak(..)`。 我稱這種模式為?*代理后置*,因為代理僅被用作最后一道防線。 #### "No Such Property/Method" 一個關于JS的常見的抱怨是,在你試著訪問或設置一個對象上還不存在的屬性時,默認情況下對象不是非常具有防御性。你可能希望為一個對象預定義所有這些屬性/方法,而且在后續使用不存在的屬性名時拋出一個錯誤。 我們可以使用一個代理來達成這種想法,既可以使用?*代理前置*?也可以?*代理后置*?設計。我們將兩者都考慮一下。 ```source-js var obj = { a: 1, foo() { console.log( "a:", this.a ); } }, handlers = { get(target,key,context) { if (Reflect.has( target, key )) { return Reflect.get( target, key, context ); } else { throw "No such property/method!"; } }, set(target,key,val,context) { if (Reflect.has( target, key )) { return Reflect.set( target, key, val, context ); } else { throw "No such property/method!"; } } }, pobj = new Proxy( obj, handlers ); pobj.a = 3; pobj.foo(); // a: 3 pobj.b = 4; // Error: No such property/method! pobj.bar(); // Error: No such property/method! ``` 對于`get(..)`和`set(..)`兩者,我們僅在目標對象的屬性已經存在時才轉送操作;否則拋出錯誤。代理對象應當是進行交互的主對象,因為它攔截這些操作來提供保護。 現在,讓我們考慮一下反過來的?*代理后置*?設計: ```source-js var handlers = { get() { throw "No such property/method!"; }, set() { throw "No such property/method!"; } }, pobj = new Proxy( {}, handlers ), obj = { a: 1, foo() { console.log( "a:", this.a ); } }; // 讓 `pobj` 稱為 `obj` 的后備 Object.setPrototypeOf( obj, pobj ); obj.a = 3; obj.foo(); // a: 3 obj.b = 4; // Error: No such property/method! obj.bar(); // Error: No such property/method! ``` 在處理器如何定義的角度上,這里的?*代理后置*?設計相當簡單。與攔截`[[Get]]`和`[[Set]]`操作并僅在目標屬性存在時轉送它們不同,我們依賴于這樣一個事實:不管`[[Get]]`還是`[[Set]]`到達了我們的`pobj`后備對象,這個動作已經遍歷了整個`[[Prototype]]`鏈并且沒有找到匹配的屬性。在這時我們可以自由地、無條件地拋出錯誤。很酷,對吧? #### 代理黑入?`[[Prototype]]`?鏈 `[[Get]]`操作是`[[Prototype]]`機制被調用的主要渠道。當一個屬性不能在直接對象上找到時,`[[Get]]`會自動將操作交給`[[Prototype]]`對象。 這意味著你可以使用一個代理的`get(..)`機關來模擬或擴展這個`[[Prototype]]`機制的概念。 我們將考慮的第一種黑科技是創建兩個通過`[[Prototype]]`循環鏈接的對象(或者說,至少看起來是這樣!)。你不能實際創建一個真正循環的`[[Prototype]]`鏈,因為引擎將會拋出一個錯誤。但是代理可以假冒它! 考慮如下代碼: ```source-js var handlers = { get(target,key,context) { if (Reflect.has( target, key )) { return Reflect.get( target, key, context ); } // 假冒循環的 `[[Prototype]]` else { return Reflect.get( target[ Symbol.for( "[[Prototype]]" ) ], key, context ); } } }, obj1 = new Proxy( { name: "obj-1", foo() { console.log( "foo:", this.name ); } }, handlers ), obj2 = Object.assign( Object.create( obj1 ), { name: "obj-2", bar() { console.log( "bar:", this.name ); this.foo(); } } ); // 假冒循環的 `[[Prototype]]` 鏈 obj1[ Symbol.for( "[[Prototype]]" ) ] = obj2; obj1.bar(); // bar: obj-1 <-- 通過代理假冒 [[Prototype]] // foo: obj-1 <-- `this` 上下文環境依然被保留 obj2.foo(); // foo: obj-2 <-- 通過 [[Prototype]] ``` 注意:?為了讓事情簡單一些,在這個例子中我們沒有代理/轉送`[[Set]]`。要完整地模擬`[[Prototype]]`兼容,你會想要實現一個`set(..)`處理器,它在`[[Prototype]]`鏈上檢索一個匹配得屬性并遵循它的描述符的行為(例如,set,可寫性)。參見本系列的?*this與對象原型*。 在前面的代碼段中,`obj2`憑借`Object.create(..)`語句`[[Prototype]]`鏈接到`obj1`。但是要創建反向(循環)的鏈接,我們在`obj1`的symbol位置`Symbol.for("[[Prototype]]")`(參見第二章的“Symbol”)上創建了一個屬性。這個symbol可能看起來有些特別/魔幻,但它不是的。它只是允許我使用一個被方便地命名的屬性,這個屬性在語義上看來是與我進行的任務有關聯的。 然后,代理的`get(..)`處理器首先檢查一個被請求的`key`是否存在于代理上。如果每個有,操作就被手動地交給存儲在`target`的`Symbol.for("[[Prototype]]")`位置中的對象引用。 這種模式的一個重要優點是,在`obj1`和`obj2`之間建立循環關系幾乎沒有入侵它們的定義。雖然前面的代碼段為了簡短而將所有的步驟交織在一起,但是如果你仔細觀察,代理處理器的邏輯完全是范用的(不具體地知道`obj1`或`obj2`)。所以,這段邏輯可以抽出到一個簡單的將它們連在一起的幫助函數中,例如`setCircularPrototypeOf(..)`。我們將此作為一個練習留給讀者。 現在我們看到了如何使用`get(..)`來模擬一個`[[Prototype]]`鏈接,但讓我們將這種黑科技推動的遠一些。與其制造一個循環`[[Prototype]]`,搞一個多重`[[Prototype]]`鏈接(也就是“多重繼承”)怎么樣?這看起來相當直白: ```source-js var obj1 = { name: "obj-1", foo() { console.log( "obj1.foo:", this.name ); }, }, obj2 = { name: "obj-2", foo() { console.log( "obj2.foo:", this.name ); }, bar() { console.log( "obj2.bar:", this.name ); } }, handlers = { get(target,key,context) { if (Reflect.has( target, key )) { return Reflect.get( target, key, context ); } // 假冒多重 `[[Prototype]]` else { for (var P of target[ Symbol.for( "[[Prototype]]" ) ]) { if (Reflect.has( P, key )) { return Reflect.get( P, key, context ); } } } } }, obj3 = new Proxy( { name: "obj-3", baz() { this.foo(); this.bar(); } }, handlers ); // 假冒多重 `[[Prototype]]` 鏈接 obj3[ Symbol.for( "[[Prototype]]" ) ] = [ obj1, obj2 ]; obj3.baz(); // obj1.foo: obj-3 // obj2.bar: obj-3 ``` 注意:?正如在前面的循環`[[Prototype]]`例子后的注意中提到的,我們沒有實現`set(..)`處理器,但對于一個將`[[Set]]`模擬為普通`[[Prototype]]`行為的解決方案來說,它將是必要的。 `obj3`被設置為多重委托到`obj1`和`obj2`。在`obj2.baz()`中,`this.foo()`調用最終成為從`obj1`中抽出`foo()`(先到先得,雖然還有一個在`obj2`上的`foo()`)。如果我們將連接重新排列為`obj2, obj1`,那么`obj2.foo()`將被找到并使用。 同理,`this.bar()`調用沒有在`obj1`上找到`bar()`,所以它退而檢查`obj2`,這里找到了一個匹配。 `obj1`和`obj2`代表`obj3`的兩個平行的`[[Prototype]]`鏈。`obj1`和/或`obj2`自身可以擁有委托至其他對象的普通`[[Prototype]]`,或者自身也可以是多重委托的代理(就像`obj3`一樣)。 正如先前的循環`[[Prototype]]`的例子一樣,`obj1`,`obj2`和`obj3`的定義幾乎完全與處理多重委托的范用代理邏輯相分離。定義一個`setPrototypesOf(..)`(注意那個“s”!)這樣的工具將是小菜一碟,它接收一個主對象和一組模擬多重`[[Prototype]]`鏈接用的對象。同樣,我們將此作為練習留給讀者。 希望在這種種例子之后代理的力量現在變得明朗了。代理使得許多強大的元編程任務成為可能。 ## `Reflect`?API `Reflect`對象是一個普通對象(就像`Math`),不是其他內建原生類型那樣的函數/構造器。 它持有對應于你可以控制的各種元編程任務的靜態函數。這些函數與代理可以定義的處理器方法(*機關*)一一對應。 這些函數中的一些看起來與在`Object`上的同名函數很相似: * `Reflect.getOwnPropertyDescriptor(..)` * `Reflect.defineProperty(..)` * `Reflect.getPrototypeOf(..)` * `Reflect.setPrototypeOf(..)` * `Reflect.preventExtensions(..)` * `Reflect.isExtensible(..)` 這些工具一般與它們的`Object.*`對等物的行為相同。但一個區別是,`Object.*`對等物在它們的第一個參數值(目標對象)還不是對象的情況下,試圖將它強制轉換為一個對象。`Reflect.*`方法在同樣的情況下僅簡單地拋出一個錯誤。 一個對象的鍵可以使用這些工具訪問/檢測: * `Reflect.ownKeys(..)`:返回一個所有直屬(不是“繼承的”)鍵的列表,正如被?`Object.getOwnPropertyNames(..)`和`Object.getOwnPropertySymbols(..)`返回的那樣。關于鍵的順序問題,參見“屬性枚舉順序”一節。 * `Reflect.enumerate(..)`:返回一個產生所有(直屬和“繼承的”)非symbol、可枚舉的鍵的迭代器(參見本系列的?*this與對象原型*)。 實質上,這組鍵與在`for..in`循環中被處理的那一組鍵是相同的。關于鍵的順序問題,參見“屬性枚舉順序”一節。 * `Reflect.has(..)`:實質上與用于檢查一個屬性是否存在于一個對象或它的`[[Prototype]]`鏈上的`in`操作符相同。例如,`Reflect.has(o,"foo")`實質上實施`"foo" in o`。 函數調用和構造器調用可以使用這些工具手動地實施,與普通的語法(例如,`(..)`和`new`)分開: * `Reflect.apply(..)`:例如,`Reflect.apply(foo,thisObj,[42,"bar"])`使用`thisObj`作為`foo(..)`函數的`this`來調用它,并傳入參數值`42`和`"bar"`。 * `Reflect.construct(..)`:例如,`Reflect.construct(foo,[42,"bar"])`實質上調用`new foo(42,"bar")`。 對象屬性訪問,設置,和刪除可以使用這些工具手動實施: * `Reflect.get(..)`:例如,`Reflect.get(o,"foo")`會取得`o.foo`。 * `Reflect.set(..)`:例如,`Reflect.set(o,"foo",42)`實質上實施`o.foo = 42`。 * `Reflect.deleteProperty(..)`:例如,`Reflect.deleteProperty(o,"foo")`實質上實施`delete o.foo`。 `Reflect`的元編程能力給了你可以模擬各種語法特性的程序化等價物,暴露以前隱藏著的抽象操作。例如,你可以使用這些能力來擴展?*領域特定語言*(DSL)的特性和API。 ### 屬性順序 在ES6之前,羅列一個對象的鍵/屬性的順序沒有在語言規范中定義,而是依賴于具體實現的。一般來說,大多數引擎會以創建的順序來羅列它們,雖然開發者們已經被強烈建議永遠不要依仗這種順序。 在ES6中,羅列直屬屬性的屬性是由`[[OwnPropertyKeys]]`算法定義的(ES6語言規范,9.1.12部分),它產生所有直屬屬性(字符串或symbol),不論其可枚舉性。這種順序僅對`Reflect.ownKeys(..)`有保證()。 這個順序是: 1. 首先,以數字上升的順序,枚舉所有數字索引的直屬屬性。 2. 然后,以創建順序枚舉剩下的直屬字符串屬性名。 3. 最后,以創建順序枚舉直屬symbol屬性。 考慮如下代碼: ```source-js var o = {}; o[Symbol("c")] = "yay"; o[2] = true; o[1] = true; o.b = "awesome"; o.a = "cool"; Reflect.ownKeys( o ); // [1,2,"b","a",Symbol(c)] Object.getOwnPropertyNames( o ); // [1,2,"b","a"] Object.getOwnPropertySymbols( o ); // [Symbol(c)] ``` 另一方面,`[[Enumeration]]`算法(ES6語言規范,9.1.11部分)從目標對象和它的`[[Prototype]]`鏈中僅產生可枚舉屬性。它被用于`Reflect.enumerate(..)`和`for..in`。可觀察到的順序是依賴于具體實現的,語言規范沒有控制它。 相比之下,`Object.keys(..)`調用`[[OwnPropertyKeys]]`算法來得到一個所有直屬屬性的列表。但是,它過濾掉了不可枚舉屬性,然后特別為了`JSON.stringify(..)`和`for..in`而將這個列表重排,以匹配遺留的、依賴于具體實現的行為。所以通過擴展,這個順序?*也*?與`Reflect.enumerate(..)`的順序像吻合。 換言之,所有四種機制(`Reflect.enumerate(..)`,`Object.keys(..)`,`for..in`,和`JSON.stringify(..)`)都同樣將與依賴于具體實現的順序像吻合,雖然技術上它們是以不同的方式達到的同樣的效果。 具體實現可以將這四種機制與`[[OwnPropertyKeys]]`的順序相吻合,但不是必須的。無論如何,你將很可能從它們的行為中觀察到以下的排序: ```source-js var o = { a: 1, b: 2 }; var p = Object.create( o ); p.c = 3; p.d = 4; for (var prop of Reflect.enumerate( p )) { console.log( prop ); } // c d a b for (var prop in p) { console.log( prop ); } // c d a b JSON.stringify( p ); // {"c":3,"d":4} Object.keys( p ); // ["c","d"] ``` 這一切可以歸納為:在ES6中,根據語言規范`Reflect.ownKeys(..)`,`Object.getOwnPropertyNames(..)`,和`Object.getOwnPropertySymbols(..)`保證都有可預見和可靠的順序。所以依賴于這種順序來建造代碼是安全的。 `Reflect.enumerate(..)`,`Object.keys(..)`,和`for..in`?(擴展一下的話還有`JSON.stringify(..)`)繼續互相共享一個可觀察的順序,就像它們往常一樣。但這個順序不一定與`Reflect.ownKeys(..)`的相同。在使用它們依賴于具體實現的順序時依然應當小心。 ## 特性測試 什么是特性測試?它是一種由你運行來判定一個特性是否可用的測試。有些時候,這種測試不僅是為了判定存在性,還是為判定對特定行為的適應性 —— 特性可能存在但有bug。 這是一種元編程技術 —— 測試你程序將要運行的環境然后判定你的程序應當如何動作。 在JS中特性測試最常見的用法是檢測一個API的存在性,而且如果它不存在,就定義一個填補(見第一章)。例如: ```source-js if (!Number.isNaN) { Number.isNaN = function(x) { return x !== x; }; } ``` 在這個代碼段中的`if`語句就是一個元編程:我們探測我們的程序和它的運行時環境,來判定我們是否和如何進行后續處理。 但是如何測試一個涉及新語法的特性呢? 你可能會嘗試這樣的東西: ```source-js try { a = () => {}; ARROW_FUNCS_ENABLED = true; } catch (err) { ARROW_FUNCS_ENABLED = false; } ``` 不幸的是,這不能工作,因為我們的JS程序是要被編譯的。因此,如果引擎還沒有支持ES6箭頭函數的話,它就會在`() => {}`語法的地方熄火。你程序中的語法錯誤會阻止它的運行,進而阻止你程序根據特性是否被支持而進行后續的不同相應。 為了圍繞語法相關的特性進行特性測試的元編程,我們需要一個方法將測試與我們程序將要通過的初始編譯步驟隔離開。舉例來說,如果我們能夠將進行測試的代碼存儲在一個字符串中,之后JS引擎默認地將不會嘗試編譯這個字符串中的內容,直到我們要求它這么做。 你的思路是不是跳到了使用`eval(..)`? 別這么著急。看看本系列的?*作用域與閉包*?來了解一下為什么`eval(..)`是一個壞主意。但是有另外一個缺陷較少的選項:`Function(..)`構造器。 考慮如下代碼: ```source-js try { new Function( "( () => {} )" ); ARROW_FUNCS_ENABLED = true; } catch (err) { ARROW_FUNCS_ENABLED = false; } ``` 好了,現在我們判定一個像箭頭函數這樣的特性是否?*能*?被當前的引擎所編譯來進行元編程。你可能會想知道,我們要用這種信息做什么? 檢查API的存在性,并定義后備的API填補,對于特性檢測成功或失敗來說都是一條明確的道路。但是對于從`ARROW_FUNCS_ENABLED`是`true`還是`false`中得到的信息來說,我們能對它做什么呢? 因為如果引擎不支持一種特性,它的語法就不能出現在一個文件中,所以你不能在這個文件中定義使用這種語法的函數。 你所能做的是,使用測試來判定你應當加載哪一組JS文件。例如,如果在你的JS應用程序中的啟動裝置中有一組這樣的特性測試,那么它就可以測試環境來判定你的ES6代碼是否可以直接加載運行,或者你是否需要加載一個代碼的轉譯版本(參見第一章)。 這種技術稱為?*分割投遞*。 事實表明,你使用ES6編寫的JS程序有時可以在ES6+瀏覽器中完全“原生地”運行,但是另一些時候需要在前ES6瀏覽器中運行轉譯版本。如果你總是加載并使用轉譯代碼,即便是在新的ES6兼容環境中,至少是有些情況下你運行的也是次優的代碼。這并不理想。 分割投遞更加復雜和精巧,但對于你編寫的代碼和你的程序所必須在其中運行的瀏覽器支持的特性之間,它代表一種更加成熟和健壯的橋接方式。 ### FeatureTests.io 為所有的ES6+語法以及語義行為定義特性測試,是一項你可能不想自己解決的艱巨任務。因為這些測試要求動態編譯(`new Function(..)`),這會產生不幸的性能損耗。 另外,在每次你的應用運行時都執行這些測試可能是一種浪費,因為平均來說一個用戶的瀏覽器在幾周之內至多只會更新一次,而即使是這樣,新特性也不一定會在每次更新中都出現。 最終,管理一個對你特定代碼庫進行的特性測試列表 —— 你的程序將很少用到ES6的全部 —— 是很容易失控而且易錯的。 “[https://featuretests.io”的“特性測試服務”為這種挫折提供了解決方案。](https://featuretests.io) 你可以將這個服務的庫加載到你的頁面中,而它會加載最新的測試定義并運行所有的特性測試。在可能的情況下,它將使用Web Worker的后臺處理中這樣做,以降低性能上的開銷。它還會使用LocalStorage持久化來緩存測試的結果 —— 以一種可以被所有你訪問的使用這個服務的站點所共享的方式,這將及大地降低測試需要在每個瀏覽器實例上運行的頻度。 你可以在每一個用戶的瀏覽器上進行運行時特性測試,而且你可以使用這些測試結果動態地向用戶傳遞最適合他們環境的代碼(不多也不少)。 另外,這個服務還提供工具和API來掃描你的文件以判定你需要什么特性,這樣你就能夠完全自動化你的分割投遞構建過程。 對ES6的所有以及未來的部分進行特性測試,以確保對于任何給定的環境都只有最佳的代碼會被加載和運行 —— FeatureTests.io使這成為可能。 ## 尾部調用優化(TCO) 通常來說,當從一個函數內部發起對另一個函數的調用時,就會分配一個?*棧幀*?來分離地管理這另一個函數調用的變量/狀態。這種分配不僅花費一些處理時間,還會消耗一些額外的內存。 一個調用棧鏈從一個函數到另一個再到另一個,通常至多擁有10-15跳。在這些場景下,內存使用不太可能是某種實際問題。 然而,當你考慮遞歸編程(一個函數頻繁地調用自己) —— 或者使用兩個或更多的函數相互調用而構成相互遞歸 —— 調用棧就可能輕易地到達上百,上千,或更多層的深度。如果內存的使用無限制地增長下去,你可能看到了它將導致的問題。 JavaScript引擎不得不設置一個隨意的限度來防止這樣的編程技術耗盡瀏覽器或設備的內存。這就是為什么我們會在到達這個限度時得到令人沮喪的“RangeError: Maximum call stack size exceeded”。 警告:?調用棧深度的限制是不由語言規范控制的。它是依賴于具體實現的,而且將會根據瀏覽器和設備不同而不同。你絕不應該帶著可精確觀察到的限度的強烈臆想進行編碼,因為它們還很可能在每個版本中變化。 一種稱為?*尾部調用*?的特定函數調用模式,可以以一種避免額外的棧幀分配的方法進行優化。如果額外的分配可以被避免,那么就沒有理由隨意地限制調用棧的深度,這樣引擎就可以讓它們沒有邊界地運行下去。 一個尾部調用是一個帶有函數調用的`return`語句,除了返回它的值,函數調用之后沒有任何事情需要發生。 這種優化只能在`strict`模式下進行。又一個你總是應該用`strict`編寫所有代碼的理由! 這個函數調用?*不是*?在尾部: ```source-js "use strict"; function foo(x) { return x * 2; } function bar(x) { // 不是一個尾部調用 return 1 + foo( x ); } bar( 10 ); // 21 ``` 在`foo(x)`調用完成后必須進行`1 + ..`,所以那個`bar(..)`調用的狀態需要被保留。 但是下面的代碼段中展示的`foo(..)`和`bar(..)`都是位于尾部,因為它們都是在自身代碼路徑上(除了`return`以外)發生的最后一件事: ```source-js "use strict"; function foo(x) { return x * 2; } function bar(x) { x = x + 1; if (x > 10) { return foo( x ); } else { return bar( x + 1 ); } } bar( 5 ); // 24 bar( 15 ); // 32 ``` 在這個程序中,`bar(..)`明顯是遞歸,但`foo(..)`只是一個普通的函數調用。這兩個函數調用都位于?*恰當的尾部位置*。`x + 1`在`bar(..)`調用之前被求值,而且不論這個調用何時完成,所有將要放生的只有`return`。 這些形式的恰當尾部調用(Proper Tail Calls —— PTC)是可以被優化的 —— 稱為尾部調用優化(TCO)—— 于是額外的棧幀分配是不必要的。與為下一個函數調用創建新的棧幀不同,引擎會重用既存的棧幀。這能夠工作是因為一個函數不需要保留任何當前狀態 —— 在PTC之后的狀態下不會發生任何事情。 TCO意味著調用棧可以有多深實際上是沒有限度的。這種技巧稍稍改進了一般程序中的普通函數調用,但更重要的是它打開了一扇大門:可以使用遞歸表達程序,即使它的調用棧深度有成千上萬層。 我們不再局限于單純地在理論上考慮用遞歸解決問題了,而是可以在真實的JavaScript程序中使用它! 作為ES6,所有的PTC都應該是可以以這種方式優化的,不論遞歸與否。 ### 重寫尾部調用 然而,障礙是只有PTC是可以被優化的;非PTC理所當然地依然可以工作,但是將造成往常那樣的棧幀分配。如果你希望優化機制啟動,就必須小心地使用PTC構造你的函數。 如果你有一個沒有用PTC編寫的函數,你可能會發現你需要手動地重新安排你的代碼,使它成為合法的TCO。 考慮如下代碼: ```source-js "use strict"; function foo(x) { if (x <= 1) return 1; return (x / 2) + foo( x - 1 ); } foo( 123456 ); // RangeError ``` 對`foo(x-1)`的調用不是一個PTC,因為在`return`之前它的結果必須被加上`(x / 2)`。 但是,要使這段代碼在一個ES6引擎中是合法的TCO,我們可以像下面這樣重寫它: ```source-js "use strict"; var foo = (function(){ function _foo(acc,x) { if (x <= 1) return acc; return _foo( (x / 2) + acc, x - 1 ); } return function(x) { return _foo( 1, x ); }; })(); foo( 123456 ); // 3810376848.5 ``` 如果你在一個實現了TCO的ES6引擎中運行前面這個代碼段,你將會如展示的那樣得到答案`3810376848.5`。然而,它仍然會在非TCO引擎中因為`RangeError`而失敗。 ### 非TCO優化 有另一種技術可以重寫代碼,讓調用棧不隨每次調用增長。 一個這樣的技術稱為?*蹦床*,它相當于讓每一部分結果表示為一個函數,這個函數要么返回另一個部分結果函數,要么返回最終結果。然后你就可以簡單地循環直到你不再收到一個函數,這時你就得到了結果。考慮如下代碼: ```source-js "use strict"; function trampoline( res ) { while (typeof res == "function") { res = res(); } return res; } var foo = (function(){ function _foo(acc,x) { if (x <= 1) return acc; return function partial(){ return _foo( (x / 2) + acc, x - 1 ); }; } return function(x) { return trampoline( _foo( 1, x ) ); }; })(); foo( 123456 ); // 3810376848.5 ``` 這種返工需要一些最低限度的改變來將遞歸抽出到`trampoline(..)`中的循環中: 1. 首先,我們將`return _foo ..`這一行包裝進函數表達式`return partial() {..`。 2. 然后我們將`_foo(1,x)`包裝進`trampoline(..)`調用。 這種技術之所以不受調用棧限制的影響,是因為每個內部的`partial(..)`函數都只是返回到`trampoline(..)`的`while`循環中,這個循環運行它然后再一次循環迭代。換言之,`partial(..)`并不遞歸地調用它自己,它只是返回另一個函數。棧的深度維持不變,所以它需要運行多久就可以運行多久。 蹦床表達的是,內部的`partial()`函數使用在變量`x`和`acc`上的閉包來保持迭代與迭代之間的狀態。它的優勢是循環的邏輯可以被抽出到一個可重用的`trampoline(..)`工具函數中,許多庫都提供這個工具的各種版本。你可以使用不同的蹦床算法在你的程序中重用`trampoline(..)`多次。 當然,如果你真的想要深度優化(于是可復用性不予考慮),你可以摒棄閉包狀態,并將對`acc`的狀態追蹤,與一個循環一起內聯到一個函數的作用域內。這種技術通常稱為?*遞歸展開*: ```source-js "use strict"; function foo(x) { var acc = 1; while (x > 1) { acc = (x / 2) + acc; x = x - 1; } return acc; } foo( 123456 ); // 3810376848.5 ``` 算法的這種表達形式很容易閱讀,而且很可能是在我們探索過的各種形式中性能最好的(嚴格地說)一個。很明顯它看起來是一個勝利者,而且你可能會想知道為什么你曾嘗試其他的方式。 這些是為什么你可能不想總是手動地展開遞歸的原因: * 與為了復用而將彈簧(循環)邏輯抽出去相比,我們內聯了它。這在僅有一個這樣的例子需要考慮時工作的很好,但只要你在程序中有五六個或更多這樣的東西時,你將很可能想要一些可復用性來將讓事情更簡短、更易管理一些。 * 這里的例子為了展示不同的形式而被故意地搞得很簡單。在現實中,遞歸算法有著更多的復雜性,比如相互遞歸(有多于一個的函數調用它自己)。 你在這條路上走得越遠,*展開*?優化就變得越復雜和越依靠手動。你很快就會失去所有可讀性的認知價值。遞歸,甚至是PTC形式的遞歸的主要優點是,它保留了算法的可讀性,并將性能優化的任務交給引擎。 如果你使用PTC編寫你的算法,ES6引擎將會實施TCO來使你的代碼運行在一個定長深度的棧中(通過重用棧幀)。你將在得到遞歸的可讀性的同時,也得到性能上的大部分好處與無限的運行長度。 ### 元? TCO與元編程有什么關系? 正如我們在早先的“特性測試”一節中講過的,你可以在運行時判定一個引擎支持什么特性。這也包括TCO,雖然判定的過程相當粗暴。考慮如下代碼: ```source-js "use strict"; try { (function foo(x){ if (x < 5E5) return foo( x + 1 ); })( 1 ); TCO_ENABLED = true; } catch (err) { TCO_ENABLED = false; } ``` 在一個非TCO引擎中,遞歸循環最終將會失敗,拋出一個被`try..catch`捕獲的異常。否則循環將由TCO輕易地完成。 討厭,對吧? 但是圍繞著TCO特性進行的元編程(或者,沒有它)如何給我們的代碼帶來好處?簡單的答案是你可以使用這樣的特性測試來決定加載一個你的應用程序的使用遞歸的版本,還是一個被轉換/轉譯為不需要遞歸的版本。 #### 自我調整的代碼 但這里有另外一種看待這個問題的方式: ```source-js "use strict"; function foo(x) { function _foo() { if (x > 1) { acc = acc + (x / 2); x = x - 1; return _foo(); } } var acc = 1; while (x > 1) { try { _foo(); } catch (err) { } } return acc; } foo( 123456 ); // 3810376848.5 ``` 這個算法試圖盡可能多地使用遞歸來工作,但是通過作用域中的變量`x`和`acc`來跟蹤這個進程。如果整個問題可以通過遞歸沒有錯誤地解決,很好。如果引擎在某一點終止了遞歸,我們簡單地使用`try..catch`捕捉它,然后從我們離開的地方再試一次。 我認為這是一種形式的元編程,因為你在運行時期間探測著引擎是否能(遞歸地)完成任務的能力,并繞過了任何可能制約你的(非TCO的)引擎的限制。 一眼(或者是兩眼!)看上去,我打賭這段代碼要比以前的版本難看許多。它運行起來還相當地慢一些(在一個非TCO環境中長時間運行的情況下)。 它主要的優勢是,除了在非TCO引擎中也能完成任意棧大小的任務外,這種對遞歸棧限制的“解法”要比前面展示的蹦床和手動展開技術靈活得多。 實質上,這種情況下的`_foo()`實際上是任意遞歸任務,甚至是相互遞歸的某種替身。剩下的內容是應當對任何算法都可以工作的模板代碼。 唯一的“技巧”是為了能夠在達到遞歸限制的事件發生時繼續運行,遞歸的狀態必須保存在遞歸函數外部的作用域變量中。我們是通過將`x`和`acc`留在`_foo()`函數外面這樣做的,而不是像早先那樣將它們作為參數值傳遞給`_foo()`。 幾乎所有的遞歸算法都可以采用這種方法工作。這意味著它是在你的程序中,進行最小的重寫就能利用TCO遞歸的最廣泛的可行方法。 這種方式仍然使用一個PTC,意味著這段代碼將會?*漸進增強*:從在一個老版瀏覽器中使用許多次循環(遞歸批處理)來運行,到在一個ES6+環境中完全利用TCO遞歸。我覺得這相當酷! ## 復習 元編程是當你將程序的邏輯轉向關注它自身(或者它的運行時環境)時進行的編程,要么為了調查它自己的結構,要么為了修改它。元編程的主要價值是擴展語言的普通機制來提供額外的能力。 在ES6以前,JavaScript已經有了相當的元編程能力,但是ES6使用了幾個新特性及大地提高了它的地位。 從對匿名函數的函數名推斷,到告訴你一個構造器是如何被調用的元屬性,你可以前所未有地在程序運行期間來調查它的結構。通用Symbols允許你覆蓋固有的行為,比如將一個對象轉換為一個基本類型值的強制轉換。代理可以攔截并自定義各種在對象上的底層操作,而且`Reflect`提供了模擬它們的工具。 特性測試,即便是對尾部調用優化這樣微妙的語法行為,將元編程的焦點從你的程序提升到JS引擎的能力本身。通過更多地了解環境可以做什么,你的程序可以在運行時將它們自己調整到最佳狀態。 你應該進行元編程嗎?我的建議是:先集中學習這門語言的核心機制是如何工作的。一旦你完全懂得了JS本身可以做什么,就是開始利用這些強大的元編程能力將這門語言向前推進的時候了!
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看