- 原文地址:Metaprogramming in ES6: Part 2 - Reflect
- 原文作者:Keith Cirkel
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:yoyoyohamapi
- 校對者:IridescentMia ParadeTo
在我的上一篇博文,我們探索了 Symbols,以及它們是如何為 JavaScript 新增了有用的超程式設計特性。這一次,我們(終於!)要開始討論反射了。如果你尚未讀過 第一部分:Symbols,那我建議你先去讀讀。在上一篇文章中,我不厭其煩地強調一點:
- Symbols 是 實現了的反射(Reflection within implementation)—— 你將 Symbols 應用到你已有的類和物件上去改變它們的行為。
- Reflect 是 通過自省(introspection)實現反射(Reflection through introspection) —— 通常用來探索非常底層的程式碼資訊。
- Proxy 是 通過調解(intercession)實現反射(Reflection through intercession) —— 包裹物件並通過自陷(trap)來攔截物件行為。
Reflect
是一個新的全域性物件(類似 JSON
或者 Math
),該物件提供了大量有用的內省(introspection)方法(內省是 “看看那個東西” 的一個非常華麗的表述)。內省工具已經存在於 JavaScript 了,例如 Object.keys
,Object.getOwnPropertyNames
等等。所以,為什麼我們仍然新的 API ,而不是直接在 Object 上做擴充套件呢?
“內建方法”
所有的 JavaScript 規範,以及因此誕生的引擎,都來源於一系列的 “內建方法”。這些內建方法能夠有效地讓 JavaScript 引擎在物件上執行一些遍佈你程式碼的基礎操作。如果你通讀了規範,你會發現這些方法散落各處,例如 [[Get]]
、[[Set]]
、[[HasOwnProperty]]
等等(如果你沒有耐心通讀所有規範,那麼這些內建方法列表在 ES5 8.12 部分 以及 ES6 9.1 部分 可以查閱到)。
其中一些 “內建方法” 對 JavaScript 程式碼是隱藏的,另一些則應用在了其他方法中,即使這些方法可用,它們仍被隱藏於難於窺見的縫隙之中。例如,Object.prototype.hasOwnProperty
是 [[HasOwnProperty]]
的一個實現,但不是所有的物件都繼承自 Object,為此,有時你不得不寫出一些古怪的程式碼才能用上 hasOwnProperty
,如下例所示:
var myObject = Object.create(null); // 這段程式碼比你想象得更加常見(尤其是在使用了新的 ES6 的類的時候)
assert(myObject.hasOwnProperty === undefined);
// 如果你想在 `myObject` 上使用 hasOwnProperty:
Object.prototype.hasOwnProperty.call(myObject, 'foo');複製程式碼
再看到另一個例子,[[OwnPropertyKeys]]
這一內建方法能獲得物件上所有的字串 key 和 Symbol key,並作為一個陣列返回。在不使用 Reflect 的情況下,能一次性獲得這些 key 的方式只有連線 Object.getOwnPropertyNames
和 Object.getOwnPropertySymbols
的結果:
var s = Symbol('foo');
var k = 'bar';
var o = { [s]: 1, [k]: 1 };
// 模擬 [[OwnPropertyKeys]]
var keys = Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o));
assert.deepEqual(keys, [k, s]);複製程式碼
反射方法
反射是一個非常有用的集合,它囊括了所有 JavaScript 引擎內部專有的 “內部方法”,現在被暴露為了一個單一、方便的物件 —— Reflect。你可能會問:“這聽起來不錯,但是為什麼不直接將內建方法繫結到 Object 上呢?就像 Object.keys
、Object.getOwnPropertyNames
這樣”。現在,我告訴你這麼做的理由:
- 反射擁有的方法不僅針對於 Object,還可能針對於函式,例如
Reflect.apply
,畢竟呼叫Object.apply(myFunction)
看起來太怪了。 - 用一個單一物件貯存內建方法能保持 JavaScript 其餘部分的純淨性,這要優於將反射方法通過點操作符掛載到建構函式或者原型上,更要優於直接使用全域性變數。
typeof
、instanceof
以及delete
已經作為反射運算子存在了 —— 為此新增同樣功能的新關鍵字將會加重開發者的負擔,同時,對於向後相容性也是一個夢魘,並且會讓 JavaScript 中的保留字數量急速膨脹。
Reflect.apply ( target, thisArgument [, argumentList] )
Reflect.apply
與 Function#apply
類似 —— 它接受一個函式,一個呼叫該函式的上下文以及一個引數陣列。從現在開始,你 可以 認為 Function#call
/Function#apply
的已經是過時版本了。這不是翻天覆地的變化,但卻有很大意義。下面展示了 Reflect.apply
的用法:
var ages = [11, 33, 12, 54, 18, 96];
// Function.prototype 風格:
var youngest = Math.min.apply(Math, ages);
var oldest = Math.max.apply(Math, ages);
var type = Object.prototype.toString.call(youngest);
// Reflect 風格:
var youngest = Reflect.apply(Math.min, Math, ages);
var oldest = Reflect.apply(Math.max, Math, ages);
var type = Reflect.apply(Object.prototype.toString, youngest);複製程式碼
從 Function.prototype.apply 到 Reflect.apply 的變遷的真正益處是防禦性:任何程式碼都能夠嘗試改變函式的 call
或者 apply
方法,這會讓你受困於崩潰的程式碼或者某些糟糕的情境。在現實世界中,這不會成為一件大事,但是下面這樣的程式碼可能真正存在:
function totalNumbers() {
return Array.prototype.reduce.call(arguments, function (total, next) {
return total + next;
}, 0);
}
totalNumbers.apply = function () {
throw new Error('Aha got you!');
}
totalNumbers.apply(null, [1, 2, 3, 4]); // 丟擲 Error('Aha got you!');
// ES5 中保證防禦性的程式碼看起來很糟糕:
Function.prototype.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;
// 你也可以這樣做,但看起來還是不夠整潔:
Function.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;
// Reflect.apply 會是救世主!
Reflect.apply(totalNumbers, null, [1, 2, 3, 4]) === 10;複製程式碼
Reflect.construct ( target, argumentsList [, constructorToCreateThis] )
類似於 Reflect.apply
—— Reflect.construct
讓你傳入一系列引數來呼叫建構函式。它能夠服務於類,並且設定正確的物件來使 Constructor 有正確的 this
引用以匹配對應的原型。在 ES5 時期,你會使用 Object.create(Constructor.prototype)
模式,然後傳遞物件到 Constructor.call
或者 Constructor.apply
。 Reflect.construct
的不同之處在於,你只需要傳遞建構函式,而不需要傳遞物件 —— Reflect.construct
處理好一切(如果省略第三個引數,那麼構造的物件原型將預設繫結到 target
引數)。在之前的風格中,完成物件構造是一件繁重的事兒,而在新的風格之下,這事兒簡單到一行程式碼即可完成:
class Greeting {
constructor(name) {
this.name = name;
}
greet() {
return Hello ${this.name};
}
}
// ES5 風格的工廠函式:
function greetingFactory(name) {
var instance = Object.create(Greeting.prototype);
Greeting.call(instance, name);
return instance;
}
// ES6 風格的工廠函式:
function greetingFactory(name) {
return Reflect.construct(Greeting, [name], Greeting);
}
// 如果省略第三個引數,那麼預設繫結物件原型到第一個引數
function greetingFactory(name) {
return Reflect.construct(Greeting, [name]);
}
// ES6 下順滑無比的線性工廠函式:
const greetingFactory = (name) => Reflect.construct(Greeting, [name]);複製程式碼
Reflect.defineProperty ( target, propertyKey, attributes )
Reflect.definedProperty
很大程度上源於 Object.defineProperty
—— 它允許你定義一個屬性的元資訊。 相較於 Object.defineProperty
,Reflect.defineProperty
要更加適合,因為 Obejct.* 暗示了它是作用在物件字面量上(畢竟 Object 是物件字面量的建構函式),然而 Reflect.defineProperty 僅只暗示了你正在做反射,這要更加的語義化。
要留心的是 Reflect.defineProperty
—— 正如 Object.defineProperty
一樣 —— 對於無效的 target
,例如 Number 或者 String 原始值(Reflect.defineProperty(1, 'foo')
),將丟擲一個 TypeError
。相較於靜默失敗,當引數型別錯誤時,丟擲錯誤以引起你的注意是一件更好的事兒。
再重複一次,你可以認為 Object.defineProperty
從現在起過時了,並使用 Reflect.defineProperty
代替:
function MyDate() {
/*…*/
}
// 老的風格下,我們使用 Object.defineProperty 來定義一個函式的屬性,顯得很奇怪
// (為什麼我們不用 Function.defineProperty ?)
Object.defineProperty(MyDate, 'now', {
value: () => currentms
});
// 新的風格下,語義就通暢得多,因為 Reflect 只是在做反射。
Reflect.defineProperty(MyDate, 'now', {
value: () => currentms
});複製程式碼
Reflect.getOwnPropertyDescriptor ( target, propertyKey )
同上面一樣,我們優先使用 Reflect.getOwnPropertyDescriptor
代替 Object.getOwnPropertyDescriptor
來獲得一個屬性的描述子元資訊。與 Object.getOwnPropertyDescriptor(1, 'foo')
會靜默失敗,返回 undefined
不同,Reflect.getOwnPropertyDescriptor(1, 'foo')
將丟擲一個 TypeError
錯誤 —— 與 Reflect.defineProperty
一樣,該錯誤是針對於 target
無效丟擲的。你現在也知道了,我們可以使用 Reflect.getOwnPropertyDescriptor
替換掉 Object.getOwnPropertyDescriptor
了:
var myObject = {};
Object.defineProperty(myObject, 'hidden', {
value: true,
enumerable: false,
});
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });
// 老的風格
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });
assert(Object.getOwnPropertyDescriptor(1, 'foo') === undefined)
Reflect.getOwnPropertyDescriptor(1, 'foo'); // throws TypeError複製程式碼
Reflect.deleteProperty ( target, propertyKey )
非常非常令人興奮,Reflect.deleteProperty
能夠刪除目標物件上的一個屬性。在 ES6 之前,你一般是通過 delete obj.foo
,現在,你可以使用 Reflect.deleteProperty(obj, 'foo')
來刪除物件屬性了。Reflect.deleteProperty
稍顯冗長,在語義上與 delete
關鍵字有些不同,但對於刪除物件卻有相同的作用。二者都是呼叫內建的 target[[Delete]](propertyKey)
方法 —— 但是 delete
運算也能 “工作” 在非物件引用上(例如變數),因此它會對傳遞給它的運算數做更多的檢查,潛在地,也就存在丟擲錯誤的可能性:
var myObj = { foo: 'bar' };
delete myObj.foo;
assert(myObj.hasOwnProperty('foo') === false);
myObj = { foo: 'bar' };
Reflect.deleteProperty(myObj, 'foo');
assert(myObj.hasOwnProperty('foo') === false);複製程式碼
再重複一遍,如果你想的話,你可以考慮使用這個 “新的方式” 來刪除屬性。這個方式顯然意圖更加明確,就是刪除屬性。
Reflect.getPrototypeOf ( target )
關於替代/淘汰 Object 方法的議題還在繼續 —— 這一次該是 Object.getPrototypeOf
了。正如其兄妹方法一樣,如果你傳入了一個諸如 Number 和 String 字面量、null
或者是 undefined
這樣無效的 target
,Reflect.getPropertyOf
將丟擲一個 TypeError
錯誤,而 Object.getPropertyOf
強制轉化 target
為一個物件 —— 所以 'a'
變為了 Object('a')
。除了語法以外,二者幾乎相同:
var myObj = new FancyThing();
assert(Reflect.getPrototypeOf(myObj) === FancyThing.prototype);
// 老的風格
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);
Object.getPrototypeOf(1); // undefined
Reflect.getPrototypeOf(1); // TypeError複製程式碼
Reflect.setPrototypeOf ( target, proto )
當然,getProtopertyOf
不能沒了 setPropertyOf
。現在,Object.setPrototypeOf
對於傳入非物件引數,將丟擲錯誤,但它會嘗試將傳入引數強制轉換為 Object,並且如果內建的 [[SetPrototype]]
操作失敗,將丟擲 TypeError
,而如果成功的話,將返回 target
引數。Reflect.setPrototypeOf
則更加簡單基礎 —— 如果其收到了一個非物件引數,它就將丟擲一個 TypeError
錯誤,但除此之外,它還會返回 [[SetPrototypeOf]]
的結果 —— 這是一個 Boolean 值,指出了操作是否錯誤。這是很有用的,因為你可以直接知曉操作錯誤與否,而不需要使用 try
/catch
,這將會俘獲其他由於引數傳遞錯誤造成的 TypeErrors
。
var myObj = new FancyThing();
assert(Reflect.setPrototypeOf(myObj, OtherThing.prototype) === true);
assert(Reflect.getPrototypeOf(myObj) === OtherThing.prototype);
// 老的風格
assert(Object.setPrototypeOf(myObj, OtherThing.prototype) === myObj);
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);
Object.setPrototypeOf(1); // TypeError
Reflect.setPrototypeOf(1); // TypeError
var myFrozenObj = new FancyThing();
Object.freeze(myFrozenObj);
Object.setPrototypeOf(myFrozenObj); // TypeError
assert(Reflect.setPrototypeOf(myFrozenObj) === false);複製程式碼
Reflect.isExtensible (target)
再一次強調這是用來替代 Object.isExtensible
的 —— 但是它比後者要更加複雜。在 ES6 之前(例如說 ES5),如果你傳入了非物件引數(typeof target !== object
),Object.isExtensible
會丟擲一個 TypeError
。ES6 則在語義上發生了改變(天哪!居然改變了現有的 API!)使得傳入非物件引數時,Object.isExtensible
返回 false
—— 因為非物件確實就是不可擴充套件。所以在 ES6 下,這個早先會丟擲錯誤的語句:Object.isExtensible(1) === false
現在表現得如你所想,語義更加準確。
上面簡短的歷史回顧引出關鍵點就是 Reflect.isExtensible
使用的是老舊行為,即當傳入非物件引數時,丟擲錯誤。我不真正確定為什麼它要這麼做,但它確實這麼做了。所以技術上 Reflect.isExtensible
改變了 Object.isExtensible
的語義,但是 Object.isExtensible
自己也發生了語義改變。下面的程式碼說明了這些:
var myObject = {};
var myNonExtensibleObject = Object.preventExtensions({});
assert(Reflect.isExtensible(myObject) === true);
assert(Reflect.isExtensible(myNonExtensibleObject) === false);
Reflect.isExtensible(1); // 丟擲 TypeError
Reflect.isExtensible(false); // 丟擲 TypeError
// 使用 Object.isExtensible
assert(Object.isExtensible(myObject) === true);
assert(Object.isExtensible(myNonExtensibleObject) === false);
// ES5 Object.isExtensible 語義
Object.isExtensible(1); // 在老版本的瀏覽器下,會丟擲 TypeError
Object.isExtensible(false); // 在老版本的瀏覽器下,會丟擲 TypeError
// ES6 Object.isExtensible 語義
assert(Object.isExtensible(1) === false); // 只工作在新的瀏覽器
assert(Object.isExtensible(false) === false); // 只工作在新的瀏覽器複製程式碼
Reflect.preventExtensions ( target )
這是最後一個反射物件從 Object 上借鑑的方法。它和 Reflect.isExtensible
有類似的故事;ES5 的 Object.preventExtensions
過去會對非物件引數丟擲錯誤,但是現在,在 ES6 中,它會返回傳入值,而 Reflect.preventExtensions
遵從的則是老的 ES5 行為 —— 即對非物件引數丟擲錯誤。另外,在操作成功的情況下,Object.preventExtensions
可能丟擲錯誤,但 Reflect.preventExtension
僅簡單地返回 true 或者 false,允許你優雅地操控失敗場景:
var myObject = {};
var myObjectWhichCantPreventExtensions = magicalVoodooProxyCode({});
assert(Reflect.preventExtensions(myObject) === true);
assert(Reflect.preventExtensions(myObjectWhichCantPreventExtensions) === false);
Reflect.preventExtensions(1); // 丟擲 TypeError
Reflect.preventExtensions(false); // 丟擲 TypeError
// 使用 Object.preventExtensions
assert(Object.preventExtensions(myObject) === true);
Object.preventExtensions(myObjectWhichCantPreventExtensions); // throws TypeError
// ES5 Object.preventExtensions 語義
Object.preventExtensions(1); // 丟擲 TypeError
Object.preventExtensions(false); // 丟擲 TypeError
// ES6 Object.preventExtensions 語義
assert(Object.preventExtensions(1) === 1);
assert(Object.preventExtensions(false) === false);複製程式碼
Reflect.enumerate ( target )
更新:在 ES2016(也稱 ES7)中,這被刪除了。
myObject[Symbol.iterator]()
是在物件 key 或者 value 上迭代的唯一方式。
最後,將引出一個全新的 Reflect 方法!Reflect.enumerate
使用了和新的 Symbol.iterator
函式(在前一章節,已對此有過討論) 一樣的語法,二者都使用了隱藏的,只有 JavaScript 引擎知道的 [[Enumerate]]
方法。換句話說,Reflect.enumerate
的唯一替代只是 myObject[Symbol.iterator()]
,只是後者可以被重寫,而前者不行。使用範例如下:
var myArray = [1, 2, 3];
myArray[Symbol.enumerate] = function () {
throw new Error('Nope!');
}
for (let item of myArray) { // error thrown: Nope!
}
for (let item of Reflect.enumerate(myArray)) {
// 1 then 2 then 3
}複製程式碼
Reflect.get ( target, propertyKey [ , receiver ])
Reflect.get
也是一個全新的方法。它是一個非常簡單的方法,其有效地呼叫了 target[propertyKey]
。如果 target
是一個非物件,函式呼叫將丟擲錯誤 —— 這是很有用的,因為目前如果你寫了 1['foo']
這樣的程式碼,它只會靜默返回 undefined
,而 Reflect.get(1, 'foo')
將丟擲一個 TypeError
錯誤!Reflect.get
一個有趣的部分是它的 receiver
引數,如果 target[propertyKey]
是一個 getter 函式,它則作為該函式的 this,例子如下所示:
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}
assert(Reflect.get(myObject, 'foo') === 1);
assert(Reflect.get(myObject, 'bar') === 2);
assert(Reflect.get(myObject, 'baz') === 3);
assert(Reflect.get(myObject, 'baz', myObject) === 3);
var myReceiverObject = {
foo: 4,
bar: 4,
};
assert(Reflect.get(myObject, 'baz', myReceiverObject) === 8);
// 非物件將丟擲錯誤
Reflect.get(1, 'foo'); // 丟擲 TypeError
Reflect.get(false, 'foo'); // 丟擲 TypeError
// 老的風格下,靜默返回 `undefined`:
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);複製程式碼
Reflect.set ( target, propertyKey, V [ , receiver ] )
你大致能夠猜出該方法是做什麼的。它是 Reflect.get
的兄弟方法,它接收另外一個引數 —— 需要被設定的值。如 Reflect.get
一樣,Reflect.set
將在傳入非物件引數時,丟擲錯誤,並且也有一個 receiver
引數指明 target[propertyKey]
為 setter 函式時使用的 this
。必須上個程式碼示例:
var myObject = {
foo: 1,
set bar(value) {
return this.foo = value;
},
}
assert(myObject.foo === 1);
assert(Reflect.set(myObject, 'foo', 2));
assert(myObject.foo === 2);
assert(Reflect.set(myObject, 'bar', 3));
assert(myObject.foo === 3);
assert(Reflect.set(myObject, 'bar', myObject) === 4);
assert(myObject.foo === 4);
var myReceiverObject = {
foo: 0,
};
assert(Reflect.set(myObject, 'bar', 1, myReceiverObject));
assert(myObject.foo === 4);
assert(myReceiverObject.foo === 1);
// 非物件將丟擲錯誤
Reflect.set(1, 'foo', {}); // 丟擲 TypeError
Reflect.set(false, 'foo', {}); // 丟擲 TypeError
// 老的風格下,靜默返回 `undefined`:
1['foo'] = {};
false['foo'] = {};
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);複製程式碼
Reflect.has ( target, propertyKey )
Reflect.has
是一個非常有趣的方法,因為它本質上與 in
運算子有一樣的功能(在迴圈之外)。二者都使用了內建的 [[HasProperty]]
,並且都會在 target
不為物件時丟擲錯誤。除非你更偏向於函式呼叫的風格,相較於 in
,沒有多少使用 Reflect.has
的理由,但是它在語言的其他方面有重要的使用,這將在下一章有清楚的講述。無論如何,先看看怎麼用它:
myObject = {
foo: 1,
};
Object.setPrototypeOf(myObject, {
get bar() {
return 2;
},
baz: 3,
});
// 不使用 Reflect.has:
assert(('foo' in myObject) === true);
assert(('bar' in myObject) === true);
assert(('baz' in myObject) === true);
assert(('bing' in myObject) === false);
// 使用 Reflect.has:
assert(Reflect.has(myObject, 'foo') === true);
assert(Reflect.has(myObject, 'bar') === true);
assert(Reflect.has(myObject, 'baz') === true);
assert(Reflect.has(myObject, 'bing') === false);複製程式碼
Reflect.ownKeys ( target )
該方法已經在本文有所提及了,你可以看到 Reflect.ownKeys
實現了 [[OwnPropertyKeys]]
,你回想一下上文的內容,你知道它連線了 Object.getOwnPropertyNames
和 Object.getOwnPropertySymbols
的結果。這讓 Reflect.ownKeys
有著不可替代的作用。下面看到用法:
var myObject = {
foo: 1,
bar: 2,
[Symbol.for('baz')]: 3,
[Symbol.for('bing')]: 4,
};
assert.deepEqual(Object.getOwnPropertyNames(myObject), ['foo', 'bar']);
assert.deepEqual(Object.getOwnPropertySymbols(myObject), [Symbol.for('baz'), Symbol.for('bing')]);
// 不使用 Reflect.ownKeys:
var keys = Object.getOwnPropertyNames(myObject).concat(Object.getOwnPropertySymbols(myObject));
assert.deepEqual(keys, ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);
// 使用 Reflect.ownKeys:
assert.deepEqual(Reflect.ownKeys(myObject), ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);複製程式碼
結論
我們對各個 Reflect 方法進行了徹底的討論。我們看到了一些現有方法的新版本,一些做了微調,一些則是完完全全新的方法 —— 這將 JavaScript 的反射提升到了一個新的層面。如果你想的話,大可以完全的拋棄 Object
.*/Function.*
方法,用 Reflect
替代之,如果你不想的話,別擔心,不用就不用,什麼都不會改變。
現在,我不想你看完兩手空空,毫無所獲。如果你想要使用 Reflect
,我們已經給予了你支援 —— 作為這個文章背後工作的一部分,我提交了一個 pull request 到 eslint,在 v1.0.0
版本,ESlint 有了一個 prefer-reflect
規則,這可以讓你在使用老舊版本的 Reflect 方法時,得到 ESLint 的提示。你也可以看下我的 eslint-config-strict 配置,該開啟 prefer-reflect
規則(也新增了許多額外的規則)。當然,如果你決定你想要使用 Reflect,你可能需要 polyfill 它;幸運的是,現在已經有了一些好的 polyfill,如 core-js 和 harmony-reflect。
對於新的 Reflect API ,你是怎麼看待的 ?計劃在你的專案中使用它了 ?可以在我的 Twitter 給我留言,我是 @keithamus。
也別忘了,這個系列的第三部分 —— 代理(Proxy)也快釋出了,我不會再拖延兩個月了。(已經發布:juejin.im/post/5a0f05…
最後,要謝謝 @mttshw 和 @WebReflection 對我工作的審視,才讓文章比預計的更加高質。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。