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
'use strict'
globalThis
window
document
其中有一些並不是真正的物件,但都是一些執行環境下支援特殊寫法。或許有一些並沒有實際作用,但可能很多人並不知道,畢竟平常大概也沒有人會這樣寫吧!所以其實也就是一些JavaScript裡無關緊要的有趣小地方。
當然...當中有一部分也有可能成為你日後會踩入的陷阱(抗)。那麼就先來說說undefined
和null
吧!
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
不過如果你連globalThis
或window
/global
也都覆寫掉了...當我沒說。也可以使用typeof
去檢查是不是undefined
。
因為類型會是
undefined
的也就只有undefiend
了
let a = undefined;
{
let undefined = "other"; // 區域變數
function isUndefined(v) {
return typeof v === 'undefined';
}
}
isUndefined(a); // -> true
在規範上,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
是大家經常弄到頭痛的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
只用在使用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
是一個很像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.target
檢查是否使使用new
操作建立新的物件,若否則會得到undefined
。
new (function () {
console.log(new.target); // > function()
})()
(function () {
console.log(new.target); // > undefined
})()
NaN
的意思是「不是個數字(Not a Number)」,但他的類型是數字。
typeof NaN; // -> "number"
對NaN
做任何數值運算都是NaN
。實際上這並不是JavaScript獨有,其實他是規範在float浮點數--二進位浮點數算術標準(IEEE 754)的一個值。
與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
代表著可以表示的最小精度。雖然文字上意思是最小的數值,但是他是大於零的。
Number.MIN_VALUE > 0; // -> true
Object.is(+0, -0); // -> false
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
實際上instanceof
是很容易被欺騙的。因為它是去尋找prototype chain上是否存在該類別,那麼就很容易模擬上面情況來建立一個同樣特殊的物件:
var obj = {};
obj instanceof Object; // -> ture, 現在還是Object實例
obj.__proto__ = null;
obj instanceof Object; // -> false, 不是Object實例了
document.all
並不是undefined
,但類型是undefined
。
document.all === undefined; // false
typeof undefined.all; // "undefined"
實際上它是一類具有[[IsHTMLDDA]]
槽的物件(Objects with an [[IsHTMLDDA]] internal slot),這類物件無法自己建立,全有執行環境提供。目前已知僅有document.all
,而且也已經被標記為廢棄,但是在主流瀏覽器依然可能可以見得著。
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
,雖然在不同執行環境下有不同但都很重要的意義。但它們都不是關鍵字,是有機會覆蓋(Shadow)的。但非常不建議這麼做。
你可能不知道,在多數瀏覽器裡的JavaScript,hashband是合法的語法,因此你可以這樣寫而不會報錯:
#! /usr/bin/env node
同樣的,其實很多JavaScript執行環境也接受HTML類型的註解。
<!-- Hello, this is a HTML comment.
'use strict'
雖然僅僅是一個合法字串,但是卻會影響到JavaScript的評估行為。
function f() {
'use strict';
console.log(this); // > undefined
}
f()
上面原本可能可以拿到globalThis
的QAQ。而且實際上受到影響的可能不只是如此。
本文同時發表於我的隨筆
需要更正一下資訊
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
感謝 sixwings 的補充。但因為目的只是點名其存在,而不是說明如何使用,所以也就不需要更正,內文我就沒有更動了。
根據the TC39 process,feature的提案分成幾個階段:
其中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了解且願意分享的話,感激不盡~