JavaScript数据类型详解
JavaScript是一门动态类型语言,它的数据类型系统包含了两大类:原始类型(Primitive Types)和引用类型(Reference Types)。理解JavaScript的类型系统对于编写高质量的代码至关重要。
数据类型概述
JavaScript中共有7种数据类型,其中包括6种原始类型和1种引用类型:
原始类型(Primitive Types)
Undefined
:表示未定义Null
:表示空对象指针Boolean
:表示真假值Number
:表示数字String
:表示文本Symbol
:表示唯一的标识符(ES6新增)
引用类型(Reference Type)
Object
:表示对象,是所有引用类型的基础
1.typeof操作符
ECMAScript的类型系统是松散的,所以需要一种手段类确定任意变量的数据类型。对一个值使用typeof
操作符会返回以下字符串。
- "undefined" 表示值未定义;
- "boolean" 表示值为布尔值;
- "string" 表示值为字符串;
- "number" 表示值为数值;
- "object" 表示值为对象(而不是函数)或null;
- "function" 表示值为函数;
- "symbol" 表示值为符号;
const message = 'Tom';
console.log(typeof message); // string
console.log(typeof(message)); // string
console.log(typeof 18); // number
注意
1.因为typeof
是操作符而不是函数,所以不需要参数(但是可以传递参数);
2.函数在ECMAScript中被认为是对象,并不代表一种数据类型。可是,函数也有自己的特殊属性,为此,就必须通过typeof
操作符来区分函数和其他对象。
2.Undefined类型
Undefined
类型只有一个值,就是特殊值undefined
。当使用var
或let
声明了变量但没有初始化时,就相当于给变量赋值了undefined
值。
let message;
console.log(message == undefined); // true
注意
一般来说,永远不用显式地给某个变量设置undefined
值。字面量undefined
主要用于比较,增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。
let message;
console.log(message); // undefined
console.log(age); // 报错
typeof(age); // undefined
对于未声明的变量,只能执行一个有用的操作,就是对它使用typeof
。返回的结果是undefined
。无论声明还是未声明,typeof
返回都是字符串'undefined'
。逻辑上是对的,虽然严格来说这两种变量存在根本上的差异,但它对任何一个变量都不可能执行任何真正的操作。
注意
即使未初始化的变量会被自动赋值为undefined
值,但仍然建议在声明变量的同时进行初始化。这样,当typeof
返回'undefined'
时,就可以知道那是因为给定的变量尚未声明,而不是变量声明了但未初始化。
undefined
是一个假值(布尔值中的false),因此,可以用更简洁的方式检测它。不过很多其他类型的值同样是假值。所以一定要明确想要检测的就是undefined
字面量,而不仅仅是假值。
let message;
if(message){
// 不会被执行
}
if(!message) {
// 会被执行
}
if(age) {
// 报错 age没有声明
}
3.Null类型
Null
类型同样只有一个值,即特殊值null
。逻辑上讲,null
值表示一个空对象指针,这也是给typeof
传一个null
会返回object
的原因。
let car = null;
console.log(typeof car); // object
在定义将来要保存对象值的变量时,建议使用null
来初始化,不要使用其他值。这样,只要检查这个变量的值是不是null
就可以知道这个变量是否在后来被重新赋予了一个对象的引用。
if(car != null) {
}
undefined
值是由null
值派生来的,因此ECMAScript将他们定义为表面上相等。
console.log(undefined == null); // true
用==
操作符,比较null
和undefined
始终返回true
。但要注意,这个操作符会为了比较而转化它的操作数。 任何时候,只要变量要保存对象,而当时又没有那个对象可以保存,就可以用null
来初始化变量,这样就可以保持null
是空对象指针的语义,并进一步将其与undefined
区分开来。
4.Boolean类型
Boolean
类型有两个字面值:true
和false
。这两个布尔值不同于数值,因此true
不等于1,false
不等于0
let found = true;
let lost = false;
注意
布尔值字面量true
和flase
是区分大小写的,因此True
和False
以及其他的大小混合形式都是有效标识符,而不是布尔值。
虽然布尔值只有两个,但所有其他类型的值都有相应布尔值的等价形式。要将一个其他类型的值转为布尔值,可以使用Boolean()
转型函数。
let message = 'Tom';
let messageBoolean = Boolean(message);
什么值能转成true
或flase
的规则取决于数据类型和实际值。
数据类型 | 转为true的值 | 转为false的值 |
---|---|---|
Boolean | true | flase |
String | 非空字符串 | ""(空字符串) |
Number | 非零数值(包括无穷值) | 0、NAN |
Object | 任意对象 | null |
Undefined | / | undefined |
上述转化非常重要,像if
等控制流语句会自动执行其他类型到布尔值的转化。
5.Number类型
Number
类型既可以表示整数也可以表示浮点数
let num1 = 10;
let num2 = 0.1;
注意
由于JavaScript保存数值的方式,实际中可能存在正零(+0)和负零(-0)。正零和负零在所有情况下都认识是等同的。
5.1浮点值
要定义浮点数,数值中必须包含小数点,而且小数点后面必须至少有一个数字。
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效但不推荐
因为储存浮点值使用的内存空间是存储整数的两倍,所以JavaScript引擎总是设法将值转成整数。在小数点后面没有数组的情况下,数值就会变成整数。类似的,如果数值本身就是整数,只是小数点后面跟着0,那它也会被转成整数。
let floatNum1 = 1.;
let floatNum2 = 10.0;
对于非常大或非常小的浮点数,可以使用科学计数法来表示。
let floatNum1 = 1.2e3; // 表示 1200 1.2 * 10的多少次幂
let floatNum2 = 1.2e-3 // 表示 0.0012
浮点值的精确度最高可达17位数,但在算数计算中远不如整数精确。例如 0.1 + 0.2 得不到0.3。由于这种微小的舍入错误,导致很难测试特定的浮点值。
注意
之所以存在这种错误,是因为使用了IEEE 745数值,这种错误并非JavaScript独有,其他使用相同格式的语言也有这种问题。
5.2值的范围
由于内存的限制,数字类型并不支持世界上所有的数值,它将最小的数值保存在Number.MIN_VALUE
中,最大值保存在Number.MAX_VALUE
中。如果某个计算得到的数值结果超过了JavaScript可以表示的范围,那么这个值会被自动转化为一个特殊的Infinity(无穷)值
。任何无法表达的负数以-Infinity
表示。任何无法表达的整数以Infinity
表示。
要确定一个值是不是有限大,可以使用isFinite()
函数
let result = Number.Max_VALUE;
console.log(isFinite(result)); // false
5.3NaN
有个特殊的数值NaN
,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是错误)。
console.log(0/0); // NaN
如果分子是非零,分母是有符号或无符号的0,则会返回Infinity
或-Infinity
。
console.log(5/0); // Infinity
console.log(-5/0); // -Infinity
NaN
有几个独特的属性,首先任何涉及NaN
的操作始终返回NaN
(NaN/10),其次NaN
不等于包括NaN
在内的所有值。
console.log(NaN == NaN); // false
JavaScript提供了一个函数isNaN()
函数,该函数接受一个参数,可以是任何数类型,然后判断这个参数是否是NaN
。
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false
console.log(isNaN("10")); // false
console.log(isNaN("Tom")); // true Tom不可以转换为数值
console.log(isNaN(true)); // true可以转成数字1 它不是NaN
注意
虽然不常见,但是isNaN()可以测试对象。此时会首先调用对象的valueOf()方法,然后再确定返回的值是否可以转化为数值。如果不能,再调用toString()方法,并测试其返回值,这通常是ECMAScript内置函数和操作符的工作方式。
5.4数值转化
有三个函数可以将非数值转化为数值:Number()
、parseInt()
、parseFloat()
。Number()
是转型函数,可以用于任何数据类型。后面两个函数主要用于将字符串转成数值。对于同样参数,这三个函数执行的操作也有区别。
Number()
函数转化规则
- 布尔值,true转化为1,false转化为0。
- 数值直接返回。
- null,返回0。
- undefined,返回NaN。
- 字符串比较复杂
- 如果字符串包含数值字符,包括数值字符串前面加、减号情况,则转化为一个十进制的数值。因此
Number("1")
返回1,Number("011")
返回11(忽略前面的零)。 - 如果字符串包含有效的浮点值格式,如
1.1
,则会转化为相应的浮点值(同样忽略前面的零)。 - 如果字符串包含有效的十六进制格式如
0xf
,则会转化为与该十六进制对应的十进制整数值。 - 如果是空字符串(不包含字符),返回0。
- 如果字符串包含除上述外的其他字符,则返回
NaN
。
- 如果字符串包含数值字符,包括数值字符串前面加、减号情况,则转化为一个十进制的数值。因此
- 对象,调用
valueOf()
方法,并按照上述规则转化返回的值,如果结果是NaN
则调用toString()
方法,在按照上述规则转化。
let num1 = Number("Tom"); // NaN
let num2 = Number(""); // 0
let num3 = Number("0001"); // 1
let num4 = Number(true); // 1
考虑到用Number()
函数转化字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用parseInt()
函数,parseInt()
函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符串开始转化。如果第一个字符不是数值字符、加号、减号,立即返回NaN
。这就意味着空字符串也会返回NaN
(这一点跟Number()不一样,它返回0),如果第一个字符是数值、加号、减号,则继续以此检测每个字符,直到字符串末尾,或碰到非数值字符。如111abc
会被转化为111
。类似的18.5
会被转化为18
。因为小数点不是有效整数字符。
假设字符串中的第一个字符是数值字符,parseInt()
函数也能识别不同整数格式(十进制、八进制、十六进制)。如果字符串以0x
开头,就会被理解为十六进制整数。如果以0
开头且紧跟着数值字符,就会被理解为八进制整数。
let num1 = parseInt("123red"); // 123
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10 解释为十六进制整数
let num4 = parseInt("18.5"); // 18
let num5 = parseInt("70"); // 70
不同的数值格式很容易混淆,因此parseInt()
也接受第二个参数,用于指定底数(进制数)。如果要知道解释的值是十六进制,可以传入第二个参数16
。
let num = parseInt("0xAF", 16); // 175
如果提供了第二个参数前面的0X
就可以去掉
let num = parseInt("AF", 16); // 175
let num = parseInt("AF"); // NaN
parseFloat()
函数的工作方式与parseInt()
函数类似,都是从0位置开始检测每个字符,解析到字符串末尾或者解析到一个无效的浮点数值为止。 parseFloat()
函数的不同之处在于,它始终忽略字符串开头的0。这个函数能识别前面讨论的所有浮点格式,以及十进制格式(开头的零始终被忽略)。十六进制数值始终返回0。因为parseFloat()
只解析十进制值,因此不能指定底数。如果字符串表示整数(没有小数点或小数点后面只有一个0),则返回整数。
let num1 = parseFloat("1235Tom"); // 1235 按照整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.1e3"); // 3100
6.String类型
String(字符串)
数据类型表示零或多个16位Unicode字符序列。字符串可以使用双引号、单引号、反引号表示。
let str1 = 'Tom';
let str2 = "Tom";
let str3 = `Tom`;
6.1字符串字面量
字面量 | 含义 |
---|---|
\n | 换行符 |
\t | 制表符 |
\b | 退格符 |
\r | 回车符 |
\f | 换页符 |
\ | 反斜杠(\) |
' | 单引号('),在字符串以单引号表示时使用,'Tom say, 'hello'' |
" | 双引号 |
` | 反引号 |
\xnn | 以十六进制编码nn表示的字符(其中n是十六进制数字0~F),\x41 等于 A |
\unnnn | 以十六进制编码nnnn表示的Unicode字符(其中n是十六进制数字0~F), \u03a3等于希腊字 Σ |
这些字符字面量可以出现在字符串的任意位置,且可以作为单个字符被解释。
let text = '你好,世界,\u03a3';
即使包含6个字符长的转义序列,变量text仍然是7个字符长。因为转义序列表示一个字符,所以只算一个长度。
字符串的长度可以通过length
属性获取
console.log(text.length); // 7
注意
如果字符串中包含双字节字符,那么length
属性返回的值可能不是准确的字符数。
6.2字符串的特点
字符串是不可变的,一旦创建,它们的值就不变了。要修改某个变量中的字符串,必须先销毁原始字符串,然后将包含新的字符串重新保存到该变量。
6.3转化为字符串
有两种方式把一个值转化为字符串。首先是几乎所有值都有的toString()
方法。这个方法唯一的用途就是返回当前值的字符串等价物。
let age = 11;
let ageString = age.toString(); // 字符串 "11"
let flag = true;
let flagString = true.toString(); // 字符串 "true"
toString()
方法可见于数值、布尔值、对象和字符串值(是的,字符串也有这个方法,该方法返回这个字符串本身的一个副本)。null
和undefined
没有这个方法。
多数情况下,toString()
不接受任何参数。不过,在对数值调用这个方法时,toString()
可以接受一个底数参数,就是告诉toString()
方法,输出的数值以什么底数表示字符串,默认情况下是十进制字符串表示,也可以是八进制、二进制、十六进制,或其他任何有效基数的字符串表示。
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
如果不确定一个值是不是null
或undefined
,可以使用String()
转型函数,它始终返回表示相应类型的字符串。下面请看转化规则。
- 如果该值有
toString()
方法,则调用该方法并返回结果。 - 如果是
null
返回 "null" - 如果是
undefined
返回"undefined"
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
提示
用加号操作符给一个值加上一个空字符串""也可以将其转化为字符串。
6.4模板字面量
ES6新增了使用模板字面量定义字符串的能力。与单引号和双引号不同,模板字面量保留换行符,可以跨行定义字符串。
let str1 = "first line \n seconde line";
let str2 = `
first line
seconde line
`;
模板字面量在定义模板时特别有用,比如下面的HTML模板
let page = `
<div>
<a href="#">
<span> Tom </span>
</a>
</div>
`;
由于模板字面量会保持反引号内部的空格,因此在使用空格的时候要注意。
6.5 字符串插值
模板字符串最常用的一个特性就是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。技术上讲,模板字面量不是字符串,而是一种特殊的JavaScript语法表达式,只不过求值后得到一个字符串。
字符串插值通过在${}
中使用一个JavaScript表达式。
const str = 'Hello';
const str2 = 'World';
// 之前的做法
const str3 = str + ' ' + str2 + ' !'; // Hello World !
// 使用字符串插值
const str4 = `${str} ${str2} !`; // Hello Wrold !
所有插入的值都会使用toString()
强制转型为字符串,而且任何JavaScript表达式都可以用于插值。嵌套的模板字符串无需转义。
console.log(`Hello ${`World`} !`);
6.6 模板字面量标签函数
模板字面量也支持定义标签函数,而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。
标签函数本身就是一个常规的函数,通过前缀到模板字面量来应用自定义行为。
let a = 3;
let b = 6;
function simpleTag(strings, aval, bval, sum) {
console.log(strings); // ["", "+", "=", ""]
console.log(aval); // 3
console.log(bval); // 6
console.log(sum); // 9
return 'foobar';
}
let untaggedResult = `${a} + ${b} = ${a + b}`;
let taggedResult = simpleTag`${a} + ${b} = ${a + b}`;
console.log(untaggedResult); // 3 + 6 = 9
console.log(taggedResult); // foobar
标签函数接收到的参数依次是原始字符串数组和对每个表达式求值的结果。这个函数的返回值是对模板字面量求值得到的字符串。
6.7 原始字符串
使用模板字面量也可以直接获取原始的模板字面量的内容,而不是被转化后的字符表示。
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
另外,也可以通过标签函数的第一个参数,即字符串数组的.raw属性取得每个字符串的原始内容
function printRaw(strings) {
for(const string of strings) {
console.log(string);
}
for(const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${ 'and'} \n`;
// ©
// 换行符
// \u00A9
// \n
7.Symbol类型
Symbol(符号)是ES6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
尽管看起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才添加的。相反符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。
7.1符号的基本用法
符号需要调用Symbol()函数初始化。因为符号本身是原始类型,所以typeof操作符对符号返回symbol。
let sym = Symbol();
console.log(typeof sym); // symbol
调用Symbol函数时,可以传入一个字符串作为描述信息,后面可以通过这个字符串来调试代码。
敲黑板!
这个字符串参数与符号定义或标识完全无关。
let sym1 = Symbol();
let sym2 = Symbol();
let sym3 = Symbol('foo');
let sym4 = Symbol('foo');
console.log(sym1 == sym2); // false
console.log(sym3 == sym4); // false
符号没有字面量语法,这也是它发挥作用的关键,按照规范,只要创建了Symbol()实例并将其作用对象的新属性,就可以保证它不会覆盖已有的对象属性、无论是符号属性还是字符串属性。
7.2使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,可以使用一个字符串作为一个键,在全局符号注册表中创建并重用符号。
为此需要使用Symbol.for()方法。
let sym = Symbol.for('foo');
console.log(typeof sym); // symbol
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,就会生成一个新符号实例添加到注册表中。后续使用相同的的字符串的调用发现存在该字符串对象的符号,就会返回符号实例。
let sym = Symbol.for('foo');
let sym2 = Symbol.for('foo');
console.log(sym == sym2); // true
即使采用相同的字符串描述,在全局注册表中定义的符号与Symbol()定义的符号也不相同。
let sym = Symbol('foo');
let sym2 = Symbol.for('foo');
console.log(sym == sym2); // false
全局注册表中的符号必须使用字符串键来创建,因此作为参数传递给Symbol.for()的任何值都会被视为字符串。如果不传参数,默认为undefined
。
let sym = Symbol.for();
let sym2 = Symbol.for('undefined');
console.log(sym == sym2); // true
可以使用Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该符号对应的字符串键。如果创建的不是全局符号,则返回undefined
。
let sym = Symbol.for('foo');
console.log(Symbol.keyFor(sym)); // foo
let sym2 = Symbol('bar');
console.log(Symbol.keyFor(sym2)); // undefined
7.3使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.definedProperties()
定义的属性。对象字面量只能在计算属性语法中使用。
let sym = Symbol('foo');
let obj = {
[sym]: 'foo symbol'
}
console.log(obj[sym]); // foo symbol
// 或者这样
obj[sym] = 'foo';
类似于Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组,这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回这两种类型的键。
let sym = Symbol('foo');
let sym2 = Symbol('bar');
let obj = {
[sym]: 'foo val',
[sym2]: 'bar val',
baz: 'baz val'
}
console.log(Object.getOwnPropertyNames(obj)); // ["baz"]
console.log(Object.getOwnPropertySymbols(obj)); // ["Symbol(foo)", "Symbol(bar)"]
console.log(Object.getOwnPropertyDescriptors(obj)); // {"baz": {...}, "Symbol(foo)": {...}, "Symbol(bar)": {...}}
console.log(Reflect.ownKeys(obj)); // ['baz', Symbol(foo), Symbol(bar)]
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会消失。但是,如果没有显示地保存对这些属性的引用,就必须遍历对象的所有符号属性才能找到相对应的属性键。
let obj = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
}
let barSymbol = Object.getOwnPropertySymbols(obj).find(symbol => symbol.toString().match(/bar/));
console.log(barSymbol); // Symbol(bar)
console.log(obj[barSymbol]); // bar val
7.4 常用内置符号
ES6引入了一批常用的内置符号,用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of
循环会在相关对象上使用Symbol.iterator属性
,那么就可以通过在自定义对象上重新定义Symbol.iterator
的值,来改变for-of
在迭代对象时的行为。
这些内置符号没什么特别之处,它们就是全局函数Symbol的普通字符串属性,指向一个符号实例,所有内置符号属性都是不可写、不可枚举、不可配置的。
注意
在提到ES6规范时,经常会引用符号在规范中的名称,前缀为@@。比如@@iterator指的就是Symbol.iterator。
7.5 Symbol.asyncIterator
根据ECMAScript规范,这个符号作为一个属性标识一个方法,该方法返回对象默认的AsyncIterator。由for-await-of语句使用。换句话说,这个符号表示实现异步迭代器API函数。
for-await-of循环会利用这个函数执行异步迭代操作。循环时,它们会调用以Symbol.asyncIterator为键的函数,并期望这个函数会返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的AsyncGenerator。
class Foo {
async *[Symbol.asyncIterator](){}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]()); // AsyncGenerator{<suspended>}
技术上,这个由Symbol.asyncIterator函数生成的对象应该通过其next()方法陆续返回Promise实例。可以通过显式调用next()方法返回,也可以隐式地通过异步生成器函数返回。
class Emitter{
constructor(max) {
this.max = max;
this.asyncIdx = 0;
}
async *[Symbol.asyncIterator]() {
while(this.asyncIdx < this.max) {
yield new Promise((resolve) => resolve(this.asyncIdx++));
}
}
}
async function asyncCount() {
let emitter = new Emitter(5);
for await (const x of emitter) {
console.log(x);
}
}
asyncCount(); // 0/1/2/3/4
7.6 Symbol.hasInstance
这个符号作为一个属性标识:一个方法,该方法决定一个构造器对象是否认可一个对象是它的示例,由instanceof操作符使用。
instanceof操作符可以用来确定一个对象实例的原型链上是否有原型。
function Foo() {};
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {};
let b = new Bar();
console.log(b instanceof Bar); // true
在ES6中,instanceof 操作符会使用Symbol.hasInstance函数来确定关系。以Symbol.hasInstance为键的函数会执行同样的操作,只是操作符对调了一下。
function Foo() {};
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {};
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true
这个属性定义在Function的原型上,因此默认在所有函数和类上都可以实现调用。由于instanceof操作符会在原型链上寻找这个属性的定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上面通过静态方法重新定义这个函数
class Bar {};
class Baz extends Bar{
static [Symbol.hasInstance]() {
return false;
}
}
let b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b)); // false
console.log(b instanceof Baz); // false
7.7 Symbol.isConcatSpreadable
根据ECMAScript规范,这个符号作为一个属性标识:一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素。ES6中的Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖Symbol.isConcatSpreadable的值可以修改这个行为。
数组对象默认情况下会被打平到已有的数组,false或假值会导致整个对象被追加到数组末尾。类数组对象默认情况下会被追加到数组末尾,true值或真值会导致这个类数组对象被打平到数组实例。其他不是类数组对象在Symbol.isConcatSpreadable被设置为true的情况下将被忽略。
let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(array)); // ['foo','bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array[1]]
let arrayLikeObject = {length: 1, 0: 'baz'};
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']
let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConspreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set[1]]
otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo']
7.8 Symbol.iterator
这个符号作为一个属性表示:一个方法,该方法返回对象默认的迭代器,由for-of语句使用。换句话说这个符号表示实现迭代器API的函数。
for-of循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用Symbol.iterator为键的函数,并默认这个函数会返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的Generator。
class Foo {
*[Symbol.iterator](){}
}
let f = new Foo();
console.log(f[Symbol.iterator]()); // Generator {<suspended>}
技术上,这个由Symbol.iterator函数生成的对象应该通过其next()方法陆续返回值。可以显式调用next()方法返回,也可以隐式通过生成器函数返回。
class Emitter{
constructor(max) {
this.max = max;
this.idx = 0;
}
async *[Symbol.iterator]() {
while(this.idx < this.max) {
yield this.idx++;
}
}
}
async function count() {
let emitter = new Emitter(5);
for (const x of emitter) {
console.log(x);
}
}
count(); // 0/1/2/3/4
7.9 Symbol.match
这个符号作为一个属性标识:一个正则表达式方法,该方法用正则表达式去匹配字符串。由String.prototype.match方法使用。
String.prototype.match()方法会使用以Symbol.match为键的函数来对正则表达式求值。正则表达式的原型上默认由这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数。
console.log(RegExp.prototype[Symbol.match]); // f [Symbol.match]() { [native code] }
console.log('foobar'.match(/bar/)); // ['bar', index: 3, input: 'foobar', groups: undefined]
这个方法传入非正则表达式会导致该值被转化为RegExp对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义Symbol.match函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制。
class FooMatcher {
static [Symbol.match] (target) {
return target.includes('foo');
}
}
console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false
class StringMatcher{
constructor(str) {
this.str = str;
}
[Symbol.match](target) {
return target.includes(this.str);
}
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false
7.10 Symbol.replace
这个符号作为一个属性表示:一个正则表达式方法,该方法替换一个字符串中匹配的子串,由String.prototype.replace()方法调用。
String.prototype.replace()方法会使用以String.replace为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String()方法的有效参数。
console.log(RegExp.prototype[Symbol.replace]); // f [Symbol.replace() { [native code] }]
console.log('foobarbaz'.replace(/bar/, 'qux')); // 'fooquxbaz'
这个方法传入非正则表达式值会导致该值被转化为RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.replace函数以取代默认对正则表达式求值的行为,从而让replace()方法使用非正则表达式实例。Symbol.replace函数接收两个参数,即调用replace()方法的字符串实例和替换字符串。返回的值没有限制。
class FooReplace{
static [Symbol.replace](target, replacement) {
return target.split('foo').join(replacement);
}
}
console.log('barfoobaz'.replace(FooReplace, 'qux')); // 'barquxbaz'
class StringReplacer {
constructor(str) {
this.str = str;
}
[Symbol.replace](target, replacement) {
return target.split(this.str).join(replacement);
}
}
console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux')); // 'barquxbaz'
7.11 Symbol.search
这个符号作为一个属性表示:一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引,由String.prototype.search()方法调用。String.prototype.search()方法会使用Symbol.search为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个方法的有效参数。
console.log(RegExp.prototype[Symbol.search]); // f [Symbol.search()] { [native code] }
console.log('foobar'.search(/bar/)); // 3
给这个方法传入非正则表达式值会导致该值转化为RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.search函数以取代默认对正则表达式求值的行为。Symbol.search函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制。
class FooSearch{
static [Symbol.search](target) {
return target.indexOf('foo');
}
}
console.log('foobar'.search(FooSearch)); // 0
console.log('barfoo'.search(FooSearch)); // 3
class StringSearch {
constructor(target) {
this.target = target;
}
[Symbol.search](target) {
return target.indexOf(this.target);
}
}
console.log('foobar'.search(new StringSearch('foo'))); // 0
console.log('barfoo'.search(new StringSearch('foo'))); // 3
7.12 Symbol.species
这个符号作为一个属性标识:一个函数,该函数作为创建派生对象的构造函数。这个属性在内置类型中最常用,用于内置类型实例方法的返回值暴露实例化派生对象的方法。
用String.species定义静态的捕获器(getter)方法,可以覆盖新创建实例的原型定义。
class Bar extends Array {};
class Baz extends Array {
static get [Symbol.species] () {
return Array;
}
}
let bar = new Bar();
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
bar = bar.concat('bar');
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
let baz = new Baz();
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // true
baz = baz.concat('baz');
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // false
7.13 Symbol.split
这个符号作为一个属性标识:一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串,由String.prototype.split()方法使用。String.prototype.split()方法会使用以Symbol.split为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数定义,因此所有正则表达式实例默认是这个String方法的参数。
console.log(RegExp.prototype[Symbol.split]); // f [Symbol.split]() { [native code] }
console.log('foobarbaz'.split(/bar/)); // ['foo', 'baz']
给这个方法传入非正则表达式值会导致该值被转化为RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.split函数以取代默认对正则表达式求值的行为,从而让split()方法使用正则表达式实例。Symbol.split函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制。
class FooSplitter {
static [Symbol.split](target) {
return target.split('foo');
}
}
console.log('barfoobaz'.split(FooSplitter)); // ['bar', 'baz']
class StringSplitter{
constructor(str) {
this.str = str;
}
[Symbol.split](target) {
return target.split(this.str);
}
}
console.log('barfoobaz'.split(new StringSplitter('foo'))); // ['bar', 'baz']
7.14 Symbol.toPrimitive
这个符号作为一个属性表示:一个方法,该方法将对象转化为相应的原始值,由Toprimitive抽象操作使用。很多内置操作都会尝试强制将对象转化为原始值,包括字符串、数值和未定义的原始类型。对于一个自定义对象实例,通过这个实例的Symbol.toTprimitive属性上定义一个函数可以改变默认行为。根据提供给这个函数的参数(string、number或default)可以控制返回的原始值。
class Foo {}
const foo = new Foo();
console.log(3 + foo); // '3[object Object]'
console.log(3 - foo); // NaN
console.log(String(foo)); // '[object Object]'
class Bar {
constructor() {
this[Symbol.toPrimitive] = function (hint) {
switch (hint) {
case 'number':
return 3;
case 'string':
return 'string bar';
case 'default':
return 'default bar';
}
}
}
}
const bar = new Bar();
console.log(3 + bar); // '3default bar'
console.log(3 - bar); // 0
console.log(String(bar)); // 'string bar'
7.15 Symbol.toStringTag
这个符号作为一个属性表示:一个字符串,该字符串用于创建对象的默认字符串描述,由内置方法Object.prototype.toString()调用。通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为Object。内置类型已经指定了这个值,但自定义类实例还需要明确定义。
let s = new Set();
console.log(s); // Set(0) {}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set
class Foo {};
let foo = new Foo();
console.log(foo); // Foo {}
console.log(foo.toString()); // [object Object]
console.log(foo[Symbol.toStringTag]); // undefined
class Bar {
constructor () {
this[Symbol.toStringTag] = 'Bar';
}
}
let bar = new Bar();
console.log(bar); // Bar {}
console.log(bar.toString); // [object Object]
console.log(bar[Symbol.toStringTag]); // Bar
7.16 Symbol.unscopables
这个符号作为一个属性表示:一个对象,该对象所有的以继承的属性,都会从关联对象的with环境绑定中排出。设置这个符号并让其映射对应属性的键值为true,就可以阻止该属性出现在with环境绑定中。
let obj = { foo: 'bar' };
with(obj) {
console.log(foo); // bar
}
obj[Symbol.unscopables] = {
foo: true
}
with(obj) {
console.log(foo); // 报错 ReferenceError
}
提示
不推荐使用 with
,因此也不推荐使用 Symbol.unscopables
。
8 Object类型
ECMAScript中的对象其实就是一组数据和功能的集合。对象通过new操作符后面跟对象类型的名称来创建。通过创建Object类型的实例来创建自己的对象,然后再给对象添加属性和方法。
let obj = new Object();
这个语法类似Java,但ECMAScript只要求在给构造函数提供参数时使用括号。如果没有参数,完全可以省略括号(不推荐)
let obj = new Object; // 合法 但不推荐
Object的实例本身并不是很有用,但理解与它相关的概念非常重要。类似Java中的java.lang.Object
,ECMAScript中的Object也是派生其他对象的基类。Object类型的所有属性和方法在派生的对象上同样存在。
每个Object实例都有如下方法:
- constructor: 用于创建当前对象的函数。
- hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(obj.hasOwnProperty('age'))。
- isPrototypeof(object):用于判断当前对象是否是另一个对象的原型。
- propertyIsEnumerable(propertyName):判断给定的属性是否可以使用for-in语句枚举。与hasOwnProperty()一样,属性名必须是字符串。
- toLocaleString():返回对象的字符串表示,该字符串反应对象所在本地化执行环境。
- toString();返回对象字符串表示。
- valueOf():返回对象对应的字符串、数值、或布尔值表示。通常与toString()的返回值相同。
注意
严格来讲,ECMA-262中对象的行为不一定适合所有的JavaScript对象。比如浏览器环境中的BOM和DOM对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受ECMA-262约束(它们由浏览器厂商实现),所以它们可能会也可能不会继承Object。
💬 欢迎评论!请确保您已登录 GitHub。