iT邦幫忙

2022 iThome 鐵人賽

1
Modern Web

這些那些你可能不知道我不知道的Web技術細節系列 第 33

你可能不知道在JS世界裡的特殊物件

特殊物件清單

JavaScript是一個有著龐大使用族群的程式語言,但是因為其歷史淵源和不同考量等因素下,其中有不少令人萬丈摸不著頭緒的設計。自連class都只作為保留字而無實際作用的時候,就已經有在接觸,在後續越了解越多,想想應該是能來分享一些,其中一些我知道的特殊物件。

  • undefined
  • null
  • this
  • super
  • NaN
  • Infinity
  • new
    • new.target
  • Object.prototype
    • 先有Function還是先有Object
  • Symbol
    • Symbol.for()Symbol.keyFor()
  • document.all
    • typeof document.all
  • arguments
  • hashbang
  • HTML comment
  • 'use strict'
  • globalThis
  • window
  • document

其中有一些並不是真正的物件,但都是一些執行環境下支援特殊寫法。或許有一些並沒有實際作用,但可能很多人並不知道,畢竟平常大概也沒有人會這樣寫吧!所以其實也就是一些JavaScript裡無關緊要的有趣小地方。
當然...當中有一部分也有可能成為你日後會踩入的陷阱(抗)。那麼就先來說說undefinednull吧!

undefined

undefined是一個屬於undefined的物件。(但可能不是唯一)

typeof undefined; // -> "undefined"

In all non-legacy browsers, undefined is a non-configurable, non-writable property. (Even when this is not the case, avoid overriding it.)
-- from MDN

儘管在現今主流的瀏覽器都是不可改變全域的undefined變數:

undefined =  'other value';  // 嘗試修改其值
console.log(undefined); // > undefined // 仍然是undefined

以上情況其實也很好理解,嘗試查詢全域環境物件的undefined屬性描述器設定就可以知道其configuable設定是false

Object.getOwnPropertyDescriptor(window, "undefined")["configurable"]; // -> false

有同樣情況的還有在變數祕密裡提到的情況。

但是就是這麼一個重要的undefined就只是保留字而非關鍵字

這意味著你可以用它作為函式變數或區域變數。(當然並不建議這麼寫)

(function (){
    let undefined = 'other value'; // 作為函式變數
    console.log(undefined); // > other value 
})()
{
    let undefined = 'other value'; // 作為區域變數
    console.log(undefined); // > other value 
}

當然也不建議你這麼寫:

// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined
// DON'T DO THIS

(() => {
  const undefined = "foo";
  console.log(undefined, typeof undefined); // > foo string
})();

((undefined) => {
  console.log(undefined, typeof undefined); // > foo string
})("foo");

因為可以覆蓋(Shadow)也就是說當判斷是否為undefined可能出問題:

let a = undefined;

{
    let undefined = "other"; // 區域變數
    function isUndefined(v) {
        return v === undefined;
    }
}

isUndefined(a); // -> false

當然你也可以直接從全域環境物件取得來比較:

let a = undefined;

{
    let undefined = "other"; // 區域變數
    function isUndefined(v) {
        return v === globalThis.undefined;
    }
}

isUndefined(a); // -> true

不過如果你連globalThiswindow/global也都覆寫掉了...當我沒說。也可以使用typeof去檢查是不是undefined

因為類型會是undefined的也就只有undefiend

let a = undefined;

{
    let undefined = "other"; // 區域變數
    function isUndefined(v) {
        return typeof v === 'undefined';
    }
}

isUndefined(a); // -> true

null

在規範上,null的類型屬於null。但是typeof null會得到object這是一個明確且廣為人知,不會修正的bug

typeof null; // -> "object"

不過,null不是Object的實例。

null instanceof Object; // -> false

<empty slot>

<empty slot>只有當建立Array的時候會出現:

Array(5); // -> Array(5) [ <5 empty slots> ]

this

this是大家經常弄到頭痛的Orz。

它很像動態變數,會隨者呼叫時的環境不同而有所不同...

var field = "AAA";

function showThisField() {
    console.log(this.field);
}

showThisField(); // > AAA

var obj = {
    field: "BBB", 
    showThisField
}

obj.showThisField(); // > BBB

當然你也可以使用箭頭函式或bind()等方式粘住this。更多可以看看之前寫的7天搞懂JS進階議題

super

super只用在使用class關鍵字建立類別的時候可以用。可以透過super取得繼承的父類別。

class A {
  constructor () {
    console.log("Create A"); // > Create A
  }
  method() {
    console.log("A Method"); // > A Method
  }
}

class B extends A {
  constructor () {
	super(); // > Create A
    console.log("Create B") // > Create B
  }
  
  subMethod() {
    super.method(); // > A Method
    console.log("B Method"); // > B Method
  }
}

var obj = new B();
// > Create A
// > Create B

obj.subMethod();
// > A Method
// > B Method

雖然說繼承尋找父層是由prototype chain去尋找,但非輕易不要手動改變prototype chain。有可能因為忽略,或是因為編譯優化的關係(聽說有可能),下面程式片段不一定總是和你想得一樣。

class C {
  constructor () {
    console.log("Create C"); // > Create C
  }
  method() {
    console.log("C Method"); // > C Method
  }
}

B.__proto__ === A; // -> true
B.prototype.__proto__ === A.prototype; // -> true

B.__proto__ = C;
B.__proto__ === C; // -> true
B.prototype.__proto__ = C.prototype;
B.prototype.__proto__ === C.prototype; // -> true
obj.subMethod();
// > "A Method" or "C Method"
// > B Method
obj.method();
// > "A Method" or "C Method"

arguments

arguments是一個很像Array,但不是Array的Arguments物件。

function f(arg1, arg2, arg3){
    let isArray = arguments instanceof Array; // -> false
    let typeIs = typeof arguments;  // -> "object"
    console.log(`isArray: ${isArray}; type is ${typeIs}`)
    // > is Array: false; type is object
}
f(1,2,3);

new

new除了是一個操作子外,同樣可以作為一個變數使用,但它只有在函式呼叫環境下才有作用。

new.target

可以透過new.target檢查是否使使用new操作建立新的物件,若否則會得到undefined

new (function () {
  console.log(new.target); // > function()
})()

(function () {
  console.log(new.target); // > undefined
})()

NaN

NaN的意思是「不是個數字(Not a Number)」,但他的類型是數字。

typeof NaN; // -> "number"

NaN做任何數值運算都是NaN。實際上這並不是JavaScript獨有,其實他是規範在float浮點數--二進位浮點數算術標準(IEEE 754)的一個值。

Infinity

NaN相同,Infinity同樣是在IEEE754裡規範的一個值。雖然你可以進行一些比較運算,但這不總是有意義的。

99999 < Infinity; // -> true
99999 > Infinity; // -> false
Infinity == Infinity; // -> true
(Infinity + 1) > Infinity // -> false

無限可以當作是不可數的,無限加一(Infinity + 1)還是無限(Infinity)。但這可能不是為一一個加一的值不比原本值大的數字。

雖然0乘上任意數值應為0,但是乘上Infinity會得到NaN

0 * Infinity; // -> NaN

關於無限大你可能會想知道更多

Number.MIN_VALUE > 0

Number.MIN_VALUE代表著可以表示的最小精度。雖然文字上意思是最小的數值,但是他是大於零的。

Number.MIN_VALUE > 0; // -> true

+0和-0不總是相同的

Object.is(+0, -0); // -> false

Object.prototype

Object.prototype是一個object,但不是Object的實例。

typeof Object.prototype; // -> "object"
Object.prototype instanceof Object; // false

它也是唯一一個.__proto__null的物件:

Object.prototype.__proto__ === null; // -> true

Object.prototype可以視為JS世界裡第一個物件Function是物件,物件的建構子是Function

Function instanceof Object; // -> true
Object.prototype.constructor === Object; // -> true
Object instanceof Function; // -> true
Object.prototype.constructor instanceof Function; // -> true

那麼是先有Object還是,先有Function?

instanceof是容易被欺騙的

實際上instanceof是很容易被欺騙的。因為它是去尋找prototype chain上是否存在該類別,那麼就很容易模擬上面情況來建立一個同樣特殊的物件:

var obj = {};
obj instanceof Object; // -> ture, 現在還是Object實例

obj.__proto__ = null;
obj instanceof Object; // -> false, 不是Object實例了

document.all

document.all並不是undefined,但類型是undefined

document.all === undefined; // false
typeof undefined.all; // "undefined"

實際上它是一類具有[[IsHTMLDDA]]槽的物件(Objects with an [[IsHTMLDDA]] internal slot),這類物件無法自己建立,全有執行環境提供。目前已知僅有document.all,而且也已經被標記為廢棄,但是在主流瀏覽器依然可能可以見得著。

Symbol

Symbol是一個特殊類別。如果經常在多個地方要使用同樣的Symbol,可以使用Symbol.for()

var s1 = Symbol.for('s1');
var s1_1 = Symbol.for('s1');
s1 === s1_1; // true

如果想反過來找,可以用Symbol.keyFor()

Symbol.keyFor(s1); // "s1"

globalThis、global、window、document

globalThis,還有global物件,以及window/document,雖然在不同執行環境下有不同但都很重要的意義。但它們都不是關鍵字,是有機會覆蓋(Shadow)的。但非常不建議這麼做。

hashbang

你可能不知道,在多數瀏覽器裡的JavaScript,hashband是合法的語法,因此你可以這樣寫而不會報錯:

#! /usr/bin/env node

HTML comment

同樣的,其實很多JavaScript執行環境也接受HTML類型的註解。

<!-- Hello, this is a HTML comment.

'use strict'

'use strict'雖然僅僅是一個合法字串,但是卻會影響到JavaScript的評估行為。

function f() {
    'use strict';
    console.log(this); // > undefined
}
f()

上面原本可能可以拿到globalThis的QAQ。而且實際上受到影響的可能不只是如此。

本文同時發表於我的隨筆


上一篇
2022 iThome 鐵人賽-有意思的系列(個人記錄用)
系列文
這些那些你可能不知道我不知道的Web技術細節33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
sixwings
iT邦研究生 4 級 ‧ 2022-12-09 10:53:28

需要更正一下資訊

hashbang 目前還不是 Javascript (ECMAScript) 的正式標準,目前還在 TC39 處在 stage4 階段,預計在 2023 年才會正式成為語言特性 ^[1]^[2]
雖然目前多數瀏覽器已經支援這項語法 ^[3],但還是留意這是否為正式標準?

另外使用上也必須在 script 第一行使用才行,說實在並不是一個很實用的特性 ^[4]

--

[1]: ECMAScript 双月报告:Hashbang Grammer 提案成功进入到 Stage 4
[2]: GitHub - tc39/proposal-hashbang: #! for JS
[3]: grammar: Hashbang (#!) comment syntax | Can I use... Support tables for HTML5, CSS3, etc
[4]: 词法文法 - JavaScript | MDN

lagagain iT邦新手 2 級 ‧ 2022-12-18 11:00:19 檢舉

感謝 sixwings 的補充。但因為目的只是點名其存在,而不是說明如何使用,所以也就不需要更正,內文我就沒有更動了。

根據the TC39 process,feature的提案分成幾個階段:

  1. Stage 0
  2. Stage 1
  3. Stage 2
  4. Stage 3
  5. Stage 4: Indicate that the addition is ready for inclusion in the formal ECMAScript standard

其中Stage 4又被寫成Finished Proposals。按照進程說明,隨時有可能進到下一次的ECMA-262,因此這沒什麼用的特性也就被我視為正式特性了。相比起來其實我更感興趣Pipeline Operator、Array Grouping、Decorators等更加實用的特性。不過當下在瀏覽Feature還是覺得hashbang挺特別(特別沒有用XD),覺得有趣就分享出來了~

hashbang在瀏覽器環境下,可以說幾乎是沒有用。只有在腳本環境,像是Node.js、deno下,並且要寫在檔案第一行才行,才會發揮作用。寫在其他處,只會與瀏覽器環境下一樣被當成註釋忽略(這也是我覺得有點有趣的地方,瀏覽器竟然支援...)。
並且我自己習慣的開發環境是Linux,像這樣從UNIX流傳下來的習慣是否在Windows同樣有作用還真不是太清楚XD。

我並不是非常清楚Stage 4和ECMA-262之間的關係,如果sixwings了解且願意分享的話,感激不盡~

我要留言

立即登入留言