9.值

在本章中,我们将研究 JavaScript 具有哪些值。

在本章中,我们偶尔会使用严格相等运算符。如果ab相等,a === b结果为true。具体含义在 10.运算符 详细解释。

9.1 什么是类型?

在本章中,我将类型视为值的集合。例如,类型 boolean 是集 {falsetrue} 。

9.2 JavaScript 的类型层次结构

图 6: JavaScript 类型的部分层次结构。缺少的是错误类,与基元类型相关的类等等。此图暗示了并非所有的对象都是 Object 的实例。

图 6 显示了 JavaScript 的类型层次结构。我们从该图中学到了什么?

  • JavaScript 区分两种值:原始值和对象。我们很快就会看到有什么区别。
  • 该图区分了类 Object 的对象和实例。 Object 的每个实例也是一个对象,但反之亦然。但是,实际上你在实践中遇到的所有对象都是 Object 的实例。例如,通过对象字面值创建的对象。关于该主题的更多细节在 原型链和类的对象并非 Object 的实例 中进行解释。

9.3 语言规范的类型

ECMAScript 规范已知的总共 7 种类型。这些类型的名称是(我使用 TypeScript 的名称,而不是规范的名称):

  • undefined:唯一元素undefined
  • null:唯一元素null
  • boolean:包含falsetrue元素。
  • number:所有数字的类型(例如-1233.141)。
  • string:所有字符串的类型(例如'abc')。
  • symbol:所有符号的类型(例如Symbol('My Symbol'))。
  • object:所有对象的类型(与Object不同,类Object及其子类的所有实例的类型)。

9.4 原始值与对象

规范对值进行了重要区分:

  • 原始值undefinednullbooleannumberstringsymbol类型的元素。
  • 所有其他值都是对象

与 Java 相比(启发了 JavaScript 语言),原始值不是二等公民。它们和对象之间的区别更加微妙。简而言之,它是:

  • 原始值:是 JavaScript 中的原子数据块。
    • 它们是值传递的 :当原始值分配给变量或传递给函数时,它们的内容被复制。
    • 它们按值 进行比较:比较两个原始值时,比较它们的内容。
  • 对象:是复合数据。
    • 它们是通过标识 (我的术语)传递:当对象被分配给变量或传递给函数时,它们的标识 (想一下指针)被复制。
    • 它们是通过标识 (我的术语)进行比较的:当比较两个对象时,他们的标识进行比较。

除此之外,原始值和对象非常相似:它们都具有属性 (键值条目),并且可以在相同的位置使用。

接下来,我们将更深入地研究原始值和对象。

9.4.1 原始值(简称:基元)

9.4.1.1 原始值是不可改变的

您无法更改,添加或删除基元的属性:

let str = 'abc';
assert.equal(str.length, 3);
assert.throws(
  () => { str.length = 1 },
  /^TypeError: Cannot assign to read only property 'length'/
);
9.4.1.2 原始值的值传递

基元是值传递的变量(包括参数)存储基元的内容。将原始值分配给变量或将其作为参数传递给函数时,会复制其内容。

let x = 123;
let y = x;
assert.equal(y, 123);
9.4.1.3 原始值按值 进行比较

基元按值 进行比较:当比较两个原始值时,我们比较它们的内容。

assert.equal(123 === 123, true);
assert.equal('abc' === 'abc', true);

要了解这种比较方式有什么特别之处,请继续阅读并找出对象的比较方式。

9.4.2 对象

对象的内容将会在 单实例对象 以及接下来的介绍更多细节。这里我们主要集中于其与原始值有哪些不同。

让我们首先探讨创建对象的两种常见方法:

  • 对象值:
    const obj = {
        first: 'Jane',
        last: 'Doe',
    };
    
    对象的值以花括号{}开头和结尾。它创建了一个具有两个属性的对象。第一个属性具有键'first'(字符串)和值'Jane'。第二个属性具有键'last'和值'Doe'
  • 数组值:
    const arr = ['foo', 'bar'];
    
    Array 值以方括号[]开头和结尾。它创建了一个带有两个元素 的数组:'foo''bar'。有关数组值的更多信息,请参阅 创建、读取、写入数组
9.4.2.1 默认情况下,对象是可变的

默认情况下,您可以自由更改,添加和删除对象的属性:

const obj = {};
obj.foo = 'abc'; // 添加一个属性
assert.equal(obj.foo, 'abc');
obj.foo = 'def'; // 改变某个属性的值
assert.equal(obj.foo, 'def');
9.4.2.2 对象是*通过标识传递

对象是通过标识传递 (我的术语):变量(包括参数)存储对象的标识 。对象的标识就像是 (想一下 JavaScript 引擎的共享主内存)上的对象实际数据的指针(或透明引用)。将对象分配给变量或将其作为参数传递给函数时,会复制其标识。每个对象字面值在堆上创建一个新对象并返回其标识。

const a = {}; // fresh empty object
// Pass the identity in `a` to `b`:
const b = a;
// Now `a` and `b` point to the same object
// (they “share” that object):
assert.equal(a === b, true);
// Changing `a` also changes `b`:
a.foo = 123;
assert.equal(b.foo, 123);

JavaScript 使用垃圾回收 自动管理内存:

let obj = { prop: 'value' };
obj = {};

现在obj的旧值{ prop: 'value' }垃圾 (不再使用)。在某个时间点(如果有足够的可用内存,可能永远不会), JavaScript 会自动将它进行垃圾回收 (从内存中删除)。

通过身份传递

“通过标识传递”意味着对象(透明引用)的标识按值传递。这种方法也称为“通过共享传递”

9.4.2.3 通过标识比较对象

通过标识 (我的术语)比较对象:如果两个变量包含相同的对象标识,则它们仅相等。如果它们引用具有相同内容的不同对象,则它们不相等。

const obj = {}; // fresh empty object
assert.equal(obj === obj, true); // same identity
assert.equal({} === {}, false); // different identities, same content

9.5 运算符typeofinstanceof:值的类型是什么?

typeofinstanceof这两个运算符可以确定给定值x的类型:

if (typeof x === 'string') ···
if (x instanceof Array) ···

他们有什么不同?

  • typeof 区分规范的 7 种类型(减去一个遗漏,加上一个补充)。
  • instanceof 测试哪个类创建了给定值。

经验法则:typeof用于原始值,instanceof用于对象

9.5.1 typeof

表 2: typeof 操作符的结果集

x typeof x
undefined 'undefined'
null 'object'
布尔值 'boolean'
数字 'number'
字符串 'string'
符号 'symbol'
函数 'function'
所有其他对象 'object'

表 2 列出typeof的所有结果。它们大致对应于语言规范的 7 种类型。唉,有两个不同之处,它们是语言怪癖:

  • typeof null 返回 'object' 而不是'null'。那是一个错误。不幸的是,它无法修复。 TC39 尝试这样做,但它在网络上打破了太多代码。
  • 函数的typeof应该是'object'(函数是对象)。为功能引入单独的类别令人困惑。

练习:typeof 的两个练习

  • exercises/operators/typeof_exrc.js
  • 额外:exercises/operators/is_object_test.js

9.5.2 instanceof

该运算符回答了问题:是否有一个类C创建了值x

x instanceof C

例如:

> (function() {}) instanceof Function
true
> ({}) instanceof Object
true
> [] instanceof Array
true

原始值不是任何实例:

> 123 instanceof Number
false
> '' instanceof String
false
> '' instanceof Object
false

练习:instanceof

exercises/operators/instanceof_exrc.js

9.6 类和构造函数

JavaScript 的对象原始工厂是构造函数 :如果通过 new 操作符调用它们,则返回自身的“实例”的普通函数。

ES6 引入了构造函数最好的语法,

在本书中,我可以互换地使用术语构造函数

类可以看作是将规范的单一类型 object 划分为子类型——它们给出了比规范中有限的 7 种类型更多的类型。每个类都是由它创建的对象的类型。

9.6.1 与基本类型关联的构造函数

每个基本类型(undefinednull的规范内部类型除外)都有一个关联的构造函数 (想一下类):

  • 构造函数Boolean与布尔值相关联。
  • 构造函数Number与数字相关联。
  • 构造函数String与字符串相关联。
  • 构造函数Symbol与符号相关联。

每个函数都扮演着几个角色。例如,Number

  • 可以将其用作函数并将值转换为数字:
    assert.equal(Number('123'), 123);
    
  • Number.prototype提供数字的属性。例如,方法.toString()
    assert.equal((123).toString, Number.prototype.toString);
    
  • Number是数字工具函数的命名空间/容器对象。例如:
    assert.equal(Number.isInteger(123), true);
    
  • 最后,你还可以将Number用作类并创建数字对象。这些对象与实数不同,应该避免。
    assert.notEqual(new Number(123), 123);
    assert.equal(new Number(123).valueOf(), 123);
    
9.6.1.1 包装原始值

与基本类型相关的构造函数也称为包装类型 ,因为它们提供了将原始值转换为对象的规范方法。在此过程中,原始值被“包装”在对象中。

const prim = true;
assert.equal(typeof prim, 'boolean');
assert.equal(prim instanceof Boolean, false);
const wrapped = Object(prim);
assert.equal(typeof wrapped, 'object');
assert.equal(wrapped instanceof Boolean, true);
assert.equal(wrapped.valueOf(), prim); // unwrap

包装在实践中很少有用,但它在语言规范内部使用,以提供原语属性。

9.7 在类型之间转换

JavaScript 中,有两种方法可以将值转换为其他类型:

  • 显式转换:通过String()等功能。
  • 强制 (自动转换):当操作接收到无法使用的操作数/参数时发生。

9.7.1 类型之间的显式转换

与基本类型关联的(构造)函数显式地将值转换为该类型:

> Boolean(0)
false
> Number('123')
123
> String(123)
'123'

你还可以使用Object()将值转换为对象:

> typeof Object(123)
'object'

9.7.2 强制(类型之间的自动转换)

对于许多操作,如果操作数/参数的类型不匹配,JavaScript 会自动转换它们。这种自动转换称为强制

例如,乘法运算符将其操作数强制转换为数字:

> '7' * '3'
21

许多内置函数也强制执行。例如,parseInt()将其参数强制转换为字符串(解析在第一个不是数字的字符处停止):

> parseInt(123.45)
123