第 8 章。值
目錄
購買書籍
(廣告,請勿封鎖。)

第 8 章。值

JavaScript 擁有大多數我們從程式語言中所期待的值:布林值、數字、字串、陣列等。所有 JavaScript 中的正常值都具備 屬性[9] 每個屬性都有一個 (或 名稱)和一個 。您可以將屬性視為記錄的欄位。您可以使用 點 (.) 算子存取屬性:

> var obj = {}; // create an empty object
> obj.foo = 123;  // write property
123
> obj.foo  // read property
123
> 'abc'.toUpperCase()  // call method
'ABC'

JavaScript 的類型系統

本章 概述 JavaScript 的類型系統。

JavaScript 的類型

根據 ECMAScript 語言規範第 8 章,JavaScript 只有六種類型

ECMAScript 語言類型對應到 ECMAScript 程式設計師使用 ECMAScript 語言直接操作的值。ECMAScript 語言類型為:

  • 未定義、Null
  • 布林值、字串、數字,以及
  • 物件

因此,即使建構函式據說有實例,但技術上來說並未引入新的類型。

靜態類型與動態類型

在靜態 類型語言中,變數、參數和物件的成員(JavaScript 稱之為屬性)在編譯時就具有編譯器已知的類型。編譯器可以使用該資訊執行類型檢查並最佳化已編譯的程式碼。

即使在靜態類型語言中,變數也有一個動態類型,也就是在執行時特定時間點變數值的類型。動態類型可能與靜態類型不同。例如(Java):

Object foo = "abc";

foo 的靜態類型為 Object;其動態類型為 String

JavaScript 是動態類型語言;變數的類型通常在編譯時並不知道。

靜態類型檢查與動態類型檢查

如果您 有類型資訊,您可以檢查在運算中使用的值(呼叫函式、套用運算子等)是否具有正確的類型。靜態類型檢查語言在編譯時執行這類檢查,而動態類型檢查語言則在執行時執行。一種語言可以同時是靜態類型檢查和動態類型檢查的語言。如果檢查失敗,您通常會收到某種錯誤或例外。

JavaScript 執行非常有限的動態類型檢查

> var foo = null;
> foo.prop
TypeError: Cannot read property 'prop' of null

不過,大多數情況下,事情會靜默失敗或運作。例如,如果您存取不存在的屬性,您會取得 undefined

> var bar = {};
> bar.prop
undefined

強制轉換

在 JavaScript 中,處理與類型不符的值的主要方法是將其強制轉換為正確的類型。強制轉換表示隱式類型轉換。大部分運算元都會強制轉換:

> '3' * '4'
12

JavaScript 的內建轉換機制僅支援 BooleanNumberStringObject 類型。沒有標準的方法可以將一個建構函式的實例轉換為另一個建構函式的實例。

警告

術語強類型弱類型沒有普遍有意義的定義。這些術語會被使用,但通常不正確。最好改用靜態類型靜態類型檢查等術語。

原始值與物件

JavaScript對值做了一些武斷的區分:

  • 原始值是布林值、數字、字串、nullundefined
  • 所有其他值都是物件

兩者之間的主要差異在於它們的比較方式;每個物件都有唯一的識別碼,並且僅(嚴格地)等於它自己

> var obj1 = {};  // an empty object
> var obj2 = {};  // another empty object
> obj1 === obj2
false

> var obj3 = obj1;
> obj3 === obj1
true

相反地,編碼相同值的原始值都被視為相同

> var prim1 = 123;
> var prim2 = 123;
> prim1 === prim2
true

以下兩個區段會更詳細地說明原始值和物件。

原始值

以下都是原始值(簡稱原始值

原始值具有以下特性:

以值進行比較

比較「內容」

> 3 === 3
true
> 'abc' === 'abc'
true
始終不可變

屬性無法變更、新增或移除:

> var str = 'abc';

> str.length = 1; // try to change property `length`
> str.length      // ⇒ no effect
3

> str.foo = 3; // try to create property `foo`
> str.foo      // ⇒ no effect, unknown property
undefined

(讀取未知屬性時,總是會傳回 undefined。)

一組固定的類型
您無法定義自己的原始類型。

物件

所有非原始值都是 物件。最常見的物件類型為:

  • 一般物件(建構函式 Object)可透過 物件文字 建立(請參閱 第 17 章

    {
        firstName: 'Jane',
        lastName: 'Doe'
    }

    前一個物件有兩個 屬性:屬性 firstName 的值為 'Jane',屬性 lastName 的值為 'Doe'

  • 陣列(建構函式 Array)可透過 陣列文字 建立(請參閱 第 18 章

    [ 'apple', 'banana', 'cherry' ]

    前一個陣列有三個元素,可透過數值索引存取。例如,'apple' 的索引為 0。

  • 正規表示式(建構函式 RegExp)可透過 正規表示式文字 建立(請參閱 第 19 章

    /^a+b+$/

物件具有下列特性

以參照比較

比較身分:每個物件都有自己的身分:

> ({} === {})  // two different empty objects
false

> var obj1 = {};
> var obj2 = obj1;
> obj1 === obj2
true
預設為可變更

您通常可以自由變更、新增和移除屬性(請參閱 點運算子 (.): 透過固定鍵存取屬性

> var obj = {};
> obj.foo = 123; // add property `foo`
> obj.foo
123
使用者可擴充
建構函式(請參閱 第 3 層:建構函式—實例工廠)可視為自訂類型的實作(類似其他語言中的類別)。

undefined 和 null

JavaScript 有兩個「非值」表示遺失的資訊,undefinednull

undefinednull 是唯一會導致任何類型的屬性存取產生例外狀況的值

> function returnFoo(x) { return x.foo }

> returnFoo(true)
undefined
> returnFoo(0)
undefined

> returnFoo(null)
TypeError: Cannot read property 'foo' of null
> returnFoo(undefined)
TypeError: Cannot read property 'foo' of undefined

undefined 有時也會用作表示不存在的元值。相反地,null 表示空值。例如,JSON 節點訪客 (請參閱 透過節點訪客轉換資料) 會傳回

  • undefined 以移除物件屬性或陣列元素
  • null 以將屬性或元素設定為 null

undefined 和 null 的發生

以下我們會檢閱 undefinednull 發生的各種情境。

undefined 的發生

未初始化的 變數為 undefined:

> var foo;
> foo
undefined

遺漏的 參數為 undefined:

> function f(x) { return x }
> f()
undefined

如果您讀取不存在的屬性,您會得到 undefined:

> var obj = {}; // empty object
> obj.foo
undefined

而且如果沒有明確傳回任何內容,函式 會隱含傳回 undefined:

> function f() {}
> f()
undefined

> function g() { return; }
> g()
undefined

null 的發生

檢查 undefined 或 null

在以下各節中,我們會檢閱如何分別檢查 undefinednull,或檢查是否存在任一者。

檢查 null

透過嚴格相等檢查 null:

if (x === null) ...

檢查 undefined

嚴格相等(===)是檢查 undefined正規方式:

if (x === undefined) ...

您也可以透過 typeof 算子(typeof:分類基本型別)檢查 undefined,但您通常應該使用上述方法。

檢查 undefined 或 null

大多數函式允許您透過 undefinednull 指示遺失值。檢查兩者的其中一種方法是透過明確比較:

// Does x have a value?
if (x !== undefined && x !== null) {
    ...
}
// Is x a non-value?
if (x === undefined || x === null) {
    ...
}

另一種方法是利用 undefinednull 都被視為 false 的事實(請參閱真值和假值

// Does x have a value (is it truthy)?
if (x) {
    ...
}
// Is x falsy?
if (!x) {
    ...
}

警告

false0NaN'' 也被視為 false

undefined 和 null 的歷史

單一的非值可以同時扮演 undefinednull 的角色。為什麼 JavaScript 有這兩個值?原因是歷史性的。

JavaScript 採用 Java 將值分割成基本型別和物件的方法。它也使用 Java 的「不是物件」值 null遵循 C(但不是 Java)設定的先例,如果將 null 轉換為數字,null 會變成 0:

> Number(null)
0
> 5 + null
5

請記住,第一個版本的 JavaScript 沒有例外處理。因此,未初始化的變數和遺失的屬性等例外情況必須透過值來指示。null 會是很好的選擇,但當時 Brendan Eich 想避免兩件事

  • 該值不應具有參考的含義,因為它不只是關於物件。
  • 該值不應轉換為 0,因為這會讓錯誤更難發現。

因此,Eich 將 undefined 作為額外的非值新增到語言中。它會轉換為 NaN

> Number(undefined)
NaN
> 5 + undefined
NaN

變更 undefined

undefined全域物件的屬性(因此是全域變數;請參閱 全域物件)。在 ECMAScript 3 中,您在讀取 undefined 時必須採取預防措施,因為很容易意外地變更其值。在 ECMAScript 5 中,這是不必要的,因為 undefined 是唯讀的。

為了防止變更 undefined,有兩種流行的技術(它們仍然與較舊的 JavaScript 引擎相關)

技術 1

隱藏全域 undefined(它可能有錯誤的值)

(function (undefined) {
    if (x === undefined) ...  // safe now
}());  // don’t hand in a parameter

在先前的程式碼中,undefined 保證具有正確的值,因為它是一個參數,其值並未由函式呼叫提供。

技術 2

void 0 進行比較,它總是(正確的)undefined(請參閱 void 運算子

if (x === void 0)  // always safe

基本資料型別的包裝物件

三種 基本資料型別布林、數字和字串具有對應的 建構函式:BooleanNumberString。它們的執行個體(所謂的 包裝物件)包含(包裝)基本資料型別值。建構函式可以用兩種方式使用:

  • 作為建構函式,它們會建立與其包裝的基本資料型別值在很大程度上不相容的物件

    > typeof new String('abc')
    'object'
    > new String('abc') === 'abc'
    false
  • 作為函式,它們會將值轉換為對應的基本資料型別(請參閱 用於轉換為布林、數字、字串和物件的函式)。這是建議的轉換方法

    > String(123)
    '123'

提示

避免使用包裝物件被視為最佳實務。您通常不需要它們,因為物件沒有基本資料型別無法執行的任何功能(除了變異之外)。(這與 JavaScript 從中繼承基本資料型別和物件之間差異的 Java 不同!)

包裝物件與基本資料型別不同

基本資料型別 值(例如 'abc')與包裝執行個體(例如 new String('abc'))有根本上的不同:

> typeof 'abc'  // a primitive value
'string'
> typeof new String('abc')  // an object
'object'
> 'abc' instanceof String  // never true for primitives
false
> 'abc' === new String('abc')
false

包裝實例是物件,而 JavaScript 中沒有比較物件的方法,甚至透過寬鬆相等 == 也無法比較(請參閱 相等運算子:=== 與 ==

> var a = new String('abc');
> var b = new String('abc');
> a == b
false

包裝和解開基本型別

包裝物件有一個使用案例:您想將屬性新增到基本型別值。然後,您包裝基本型別並將屬性新增到包裝物件。您需要解開值,才能使用它。

透過呼叫包裝建構函式來包裝基本型別

new Boolean(true)
new Number(123)
new String('abc')

透過呼叫方法 valueOf() 來解開基本型別。所有物件都有這個方法(如 轉換為基本型別 中所述)

> new Boolean(true).valueOf()
true
> new Number(123).valueOf()
123
> new String('abc').valueOf()
'abc'

正確地將包裝物件轉換為基本型別 會萃取出數字和字串,但不會萃取出布林值:

> Boolean(new Boolean(false))  // does not unwrap
true
> Number(new Number(123))  // unwraps
123
> String(new String('abc'))  // unwraps
'abc'

原因在 轉換為布林值 中說明。

基本型別從包裝器借用它們的方法

基本型別沒有自己的方法,而是從包裝器借用它們

> 'abc'.charAt === String.prototype.charAt
true

草率模式和嚴格模式以不同的方式處理這種借用。在草率模式中,基本型別會在執行時轉換為包裝器

String.prototype.sloppyMethod = function () {
    console.log(typeof this); // object
    console.log(this instanceof String); // true
};
''.sloppyMethod(); // call the above method

在嚴格模式中,包裝器原型中的方法會以透明的方式使用

String.prototype.strictMethod = function () {
    'use strict';
    console.log(typeof this); // string
    console.log(this instanceof String); // false
};
''.strictMethod(); // call the above method

型別強制轉換

型別強制轉換 表示將一種型別的值隱式轉換為另一種型別的值。JavaScript 的大多數運算子、函式和方法會強制轉換運算元和參數為它們需要的型別。例如,乘法運算子 (*) 的運算元會強制轉換為數字:

> '3' * '4'
12

另一個範例,如果其中一個運算元是字串,加號運算子 (+) 會將另一個運算元轉換為字串

> 3 + ' times'
'3 times'

型別強制轉換可能會隱藏錯誤

因此,JavaScript 很少會抱怨值具有錯誤的型別。例如,程式通常會接收使用者輸入(來自線上表單或 GUI 小工具)作為字串,即使使用者輸入的是數字。如果您將數字字串視為數字,您不會收到警告,只會得到意外的結果。例如

var formData = { width: '100' };

// You think formData.width is a number
// and get unexpected results
var w = formData.width;
var outer = w + 20;

// You expect outer to be 120, but it’s not
console.log(outer === 120);  // false
console.log(outer === '10020');  // true

在類似上述的情況中,您應該在早期階段轉換為適當的型別

var w = Number(formData.width);

轉換為布林值、數字、字串和物件的函式

下列函式是 將值轉換為布林值、數字、字串或物件 的首選方式:

Boolean()(請參閱 轉換為布林值

將值 轉換為布林值。下列值會轉換為 false;它們稱為「假值」:

  • undefinednull
  • false
  • 0NaN
  • ''

所有其他值都視為「真值」,並轉換為 true(包括所有物件!)。

Number()(請參閱 轉換為數字

值轉換為數字:

  • undefined 會變成 NaN
  • null 會變成 0
  • false 會變成 0true 會變成 1
  • 字串會被剖析。
  • 物件會先轉換為基本型別(稍後討論),然後再轉換為數字。
String()(請參閱 轉換為字串

將值轉換為字串。它 對所有基本型別都有明顯的結果。例如:

> String(null)
'null'
> String(123.45)
'123.45'
> String(false)
'false'

物件會 先轉換為基本型別(稍後討論),然後再轉換為字串。

Object()(請參閱 將任何值轉換為物件

將物件轉換為它們自己,undefinednull 轉換為空物件,以及將基本型別轉換為包裝的基本型別。例如:

> var obj = { foo: 123 };
> Object(obj) === obj
true

> Object(undefined)
{}
> Object('abc') instanceof String
true

請注意,Boolean()Number()String()Object() 是作為函式呼叫的。您通常不會將它們用作建構函式。然後它們會建立它們自己的執行個體(請參閱 基本型別的包裝物件)。

演算法:ToPrimitive()—將值轉換為基本型別

若要 將值轉換為數字或字串,它會先轉換為任意基本型別值,然後再轉換為最終型別(如 轉換為布林值、數字、字串和物件的函式 中所討論)。

ECMAScript 規範有一個內部函式 ToPrimitive()(無法從 JavaScript 存取),它會執行此轉換。了解 ToPrimitive() 可讓您設定如何將物件轉換為數字和字串。它的簽章如下

ToPrimitive(input, PreferredType?)

選用參數 PreferredType 指出轉換的最終類型:根據 ToPrimitive() 的結果會轉換為數字或字串,因此可能是 NumberString

如果 PreferredTypeNumber,則執行下列步驟

  1. 如果 input 是基本型別,則傳回它(沒有其他動作)。
  2. 否則,input 是物件。呼叫 input.valueOf()。如果結果是基本型別,則傳回它。
  3. 否則,呼叫 input.toString()。如果結果是基本型別,則傳回它。
  4. 否則,擲回 TypeError(指出無法將 input 轉換為基本型別)。

如果 PreferredTypeString,則步驟 2 和 3 會交換。也可以省略 PreferredType;然後它會被視為日期的 String 和所有其他值的 Number。這是運算子 +== 呼叫 ToPrimitive() 的方式。

範例:ToPrimitive() 的實際運作

預設 實作 valueOf() 傳回 this,而預設實作 toString() 傳回類型資訊:

> var empty = {};
> empty.valueOf() === empty
true
> empty.toString()
'[object Object]'

因此,Number() 會略過 valueOf() 並將 toString() 的結果轉換為數字;也就是說,它會將 '[object Object]' 轉換為 NaN

> Number({})
NaN

下列物件自訂 valueOf(),這會影響 Number(),但不會對 String() 造成任何變更

> var n = { valueOf: function () { return 123 } };
> Number(n)
123
> String(n)
'[object Object]'

下列物件自訂 toString()。因為結果可以轉換為數字,所以 Number() 可以傳回數字

> var s = { toString: function () { return '7'; } };
> String(s)
'7'
> Number(s)
7



[9] 技術上來說,基本型別沒有自己的屬性,它們會從包裝器建構函式借用屬性。但這是在幕後進行,所以您通常不會看到。

下一步:9. 運算子