Object 轉 Primitive

前言

本文從一道題目提及,使下列等式成立:html

let a;

// Do something with a...

(a == 1 && a == 2 && a == 3) === true;
複製代碼

這道題目有不少解法(見文章底部),但最爲常見的解法是經過 ES2015 的 Proxy 代理對象實現:node

const a = new Proxy(
  {},
  {
    val: 1,
    get() {
      return () => this.val++;
    },
  }
);
複製代碼

Proxy 對象的 get(),和 Object.defineProperty(obj, propertyName, { get: ... }) 的做用很是類似,但 Proxy 不須要指明 propertyName.git

此時,a 做爲空對象 {} 的代理對象,任未嘗試訪問 a 對象屬性的操做都會被 get() 方法攔截,但 a == 1 沒有顯式聲明訪問 a 的屬性,那 get() 怎麼會被執行?github

隱式轉換

雖然從字面上看不出顯式訪問,但不要忽略了一個重要條件,即非嚴格相等運算符 ==,它會將等式兩邊的值作出相應的 隱式轉換(又稱強制類型轉換) 再進行比較。算法

而隱式轉換的最終目的,是將等式兩邊的值都轉換爲 原始值(Primitive value),例如:string、number、boolean...數組

console.log(0 == false);
// true
複製代碼

實際上,若是操做數之一是 Boolean,則將布爾操做數轉換爲 1 或 0,即 Number(false) => 0,具體細則可參照 抽象相等比較算法markdown

當執行 a == 1 時,a 做爲一個 Proxy 對象,無例外地執行了隱式轉換,但轉換過程依舊無從得知。frontend

但我推測:Proxy 其本質是 Object,有可能調用了原型鏈(prototype)上的方法。函數

MDN-handler.get() 指出,get() 方法會攔截代理對象的如下操做:oop

  • 訪問自身屬性
  • 訪問原型鏈上的屬性
  • Reflect.get()

爲了驗證個人想法,打印出具體的 propertyName:

var p = new Proxy(
  {},
  {
    get: function(target, propertyName) {
      console.log(propertyName);
    },
  }
);

p + ""; // 觸發隱式轉換
複製代碼

控制檯打印出如下內容:

能夠看出,Object 在隱式轉換的過程當中,會依次訪問 Symbol(Symbol.toPrimitive)valueOftoString.

顯然,這對應了 3 個方法的名稱,即當其中任意一個方法被調用並 return 原始值時,隱式轉換成功。不然拋出錯誤 Uncaught TypeError: Cannot convert object to primitive value.

爲了簡化問題,我定義了一個普通的對象(POJO):

const obj = {};

"hello " + obj; // 觸發隱式轉換
複製代碼

首先,調用 obj[Symbol.toPrimitive](),由於我沒有顯式定義該方法,因此沒法返回原始值。

其次調用 valueOf(),它和 toString() 都位於 Object.prototype:

obj.valueOf() 返回自己 {},不是原始值,繼續調用下一個方法。

obj.toString() 返回 "[object Object]",屬於基本數據類型 string,隱式轉換成功,控制檯打印出 "hello [object Object]".

爲何返回 "[object Object]",可 查看此處

回到最初的問題,在 a 進行隱式轉換的第一步,即訪問 a[Symbol.toPrimitive] 時,被 get() 所攔截。

get() 返回 () => this.val++,返回的函數會被調用,而且 get() 中的 this 上下文綁定在 handler 對象上,最終返回 1.

const handler = {
  val: 1,
  get() {
    return () => this.val++;
  },
};

const a = new Proxy({}, handler);

a == 1; // true
a == 2; // true
a == 3; // true
複製代碼

而這一切,在 ECMAScript Language Specification - 7.1.1 ToPrimitive 中早有定義,它指明瞭 JavaScript 引擎是如何按照步驟把 object 轉換爲 primitive.

這裏的 @@toPrimitive 就是指 Symbol.toPrimitive.

注意這個 hint,它會隨着表達式和運算符的不一樣而改變,從而讓一個對象能轉化成多種原始值。

const person = {
  [Symbol.toPrimitive](hint) {
    if (hint === "string") {
      return "I am string";
    } else if (hint === "number") {
      return "I am number";
    } else if (hint === "default") {
      return "I am default";
    }
  },
};

console.log(`${person}`);
// I am string

console.log(person * 1);
// NaN

console.log(person + "");
// I am default
複製代碼

Symbol.toPrimitive 沒有被定義時,會繼續執行 OrdinaryToPrimitive 抽象操做,同時 hint 會決定 toString()valueOf() 的前後調用順序。

查看源碼

Talk is cheap. Show me the code. —— Quote by Linus Torvalds

有規範,天然就有對應的實現,經過 source.chromium.org,你能夠用 「關鍵詞搜索」 快速定位至源碼,例如搜索以前規範中出現的 「OrdinaryToPrimitive」,就能看到不少相關的代碼片斷。

查看 v8/src/objects/js-objects.cc 目錄下的 OrdinaryToPrimitive

MaybeHandle<Object> JSReceiver::OrdinaryToPrimitive( Handle<JSReceiver> receiver, OrdinaryToPrimitiveHint hint) {
  Isolate* const isolate = receiver->GetIsolate();
  Handle<String> method_names[2];
  switch (hint) {
    case OrdinaryToPrimitiveHint::kNumber:
      method_names[0] = isolate->factory()->valueOf_string();
      method_names[1] = isolate->factory()->toString_string();
      break;
    case OrdinaryToPrimitiveHint::kString:
      method_names[0] = isolate->factory()->toString_string();
      method_names[1] = isolate->factory()->valueOf_string();
      break;
  }
  for (Handle<String> name : method_names) {
    Handle<Object> method;
    ASSIGN_RETURN_ON_EXCEPTION(isolate, method,
                               JSReceiver::GetProperty(isolate, receiver, name),
                               Object);
    if (method->IsCallable()) {
      Handle<Object> result;
      ASSIGN_RETURN_ON_EXCEPTION(
          isolate, result,
          Execution::Call(isolate, method, receiver, 0, nullptr), Object);
      if (result->IsPrimitive()) return result;
    }
  }
  THROW_NEW_ERROR(isolate,
                  NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
                  Object);
}
複製代碼

懂的人天然懂,但猶如殘疾人通常 C 語言水平的我兩眼一黑,只得繼續翻看其餘文件。

很快發現 third_party/devtools-frontend/src/node_modules/es-to-primitive/es2015.js 文件一樣包含該關鍵詞,而且包含了 ToPrimitive 的具體 JS 實現。

var ordinaryToPrimitive = function OrdinaryToPrimitive(O, hint) {
  if (typeof O === "undefined" || O === null) {
    throw new TypeError("Cannot call method on " + O);
  }
  if (typeof hint !== "string" || (hint !== "number" && hint !== "string")) {
    throw new TypeError('hint must be "string" or "number"');
  }
  var methodNames =
    hint === "string" ? ["toString", "valueOf"] : ["valueOf", "toString"];
  var method, result, i;
  for (i = 0; i < methodNames.length; ++i) {
    method = O[methodNames[i]];
    if (isCallable(method)) {
      result = method.call(O);
      if (isPrimitive(result)) {
        return result;
      }
    }
  }
  throw new TypeError("No default value");
};
複製代碼
module.exports = function ToPrimitive(input) {
  if (isPrimitive(input)) {
    return input;
  }
  var hint = "default";
  if (arguments.length > 1) {
    if (arguments[1] === String) {
      hint = "string";
    } else if (arguments[1] === Number) {
      hint = "number";
    }
  }

  var exoticToPrim;
  if (hasSymbols) {
    if (Symbol.toPrimitive) {
      exoticToPrim = GetMethod(input, Symbol.toPrimitive);
    } else if (isSymbol(input)) {
      exoticToPrim = Symbol.prototype.valueOf;
    }
  }
  if (typeof exoticToPrim !== "undefined") {
    var result = exoticToPrim.call(input, hint);
    if (isPrimitive(result)) {
      return result;
    }
    throw new TypeError("unable to convert exotic object to primitive");
  }
  if (hint === "default" && (isDate(input) || isSymbol(input))) {
    hint = "string";
  }
  return ordinaryToPrimitive(input, hint === "default" ? "number" : hint);
};
複製代碼

我特地指出源代碼,是爲了證實源碼是有跡可循的,它與 ECMAScript Language Specification 密不可分。

其餘解法

巧用字符 · 障眼法

在龐大的字符庫中,選擇三個形態類似的字符 「a」,便可使等式成立。

不過,障眼法畢竟是障眼法,一旦變動爲其餘字體就會原形畢露。

let a = 1;
let a = 2;
let а = 3;

if (a == 1 && a == 2 && а == 3) {
  console.log("awesome!");
}
複製代碼

巧用數組

let a = [1, 2, 3];

a.join = a.shift;

if (a == 1 && a == 2 && a == 3) {
  console.log("awesome!");
}
複製代碼

根據 規範,數組在 ToPrimitive 時,調用Array.prototype.toString(),其內部調用了 Array.prototype.join().

Reference

相關文章
相關標籤/搜索