[譯]ES6 中的超程式設計:第二部分 —— 反射(Reflect)

吳曉軍發表於2017-11-17

在我的上一篇博文,我們探索了 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.keysObject.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.getOwnPropertyNamesObject.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.keysObject.getOwnPropertyNames 這樣”。現在,我告訴你這麼做的理由:

  1. 反射擁有的方法不僅針對於 Object,還可能針對於函式,例如 Reflect.apply,畢竟呼叫 Object.apply(myFunction) 看起來太怪了。
  2. 用一個單一物件貯存內建方法能保持 JavaScript 其餘部分的純淨性,這要優於將反射方法通過點操作符掛載到建構函式或者原型上,更要優於直接使用全域性變數。
  3. typeofinstanceof 以及 delete 已經作為反射運算子存在了 —— 為此新增同樣功能的新關鍵字將會加重開發者的負擔,同時,對於向後相容性也是一個夢魘,並且會讓 JavaScript 中的保留字數量急速膨脹。

Reflect.apply ( target, thisArgument [, argumentList] )

Reflect.applyFunction#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.applyReflect.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.definePropertyReflect.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 這樣無效的 targetReflect.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.getOwnPropertyNamesObject.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-jsharmony-reflect

對於新的 Reflect API ,你是怎麼看待的 ?計劃在你的專案中使用它了 ?可以在我的 Twitter 給我留言,我是 @keithamus

也別忘了,這個系列的第三部分 —— 代理(Proxy)也快釋出了,我不會再拖延兩個月了。(已經發布:juejin.im/post/5a0f05…

最後,要謝謝 @mttshw@WebReflection 對我工作的審視,才讓文章比預計的更加高質。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章