基础知识回顾
当前版本(ECMAScript 2021)ECMAScript 标准定义了 8 种类型:
简单数据类型(原始类型)
共 7 种:String、Number、Boolean、Undefined、Null、Symbol、BigInt 。
原始类型的值特点:值不可变,无属性无方法,保存在栈内存中、值比较。
无属性无方法可能会让人迷惑:
let s1 = "some text";
let s2 = s1.substring(2);
原始值本身不是对象,因此逻辑上不应该有方法,实际上这个例子确实没有报错,且得到了结果。原因是第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。
当以读模式访问原始值的时候,后台会执行以下三步(以上面代码为例):
- 创建一个 String 类型的实例
- 调用实例上的特定方法
- 销毁实例
等同于后台自动执行了下面的代码
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;
因此原始值拥有了对象的行为。
复杂数据类型(引用类型)
除了上面的 7 种基本数据类型外,剩下的就是引用类型,统称为 Object 类型
-
基本引用类型
-
Data和RegExp等ECMAScript 提供的原 生引用类型 -
原始值包装类型,ECMAScript 提供了 3 种特殊的引用类型:
Boolean、Number和String(只有这三种)let stringObject = new String('hello'); // 如果在使用包装对象时,忘了在前面加 “new” 操作符,会等被当做普通函数执行,作用是把任何类型的数据转换为对应的类型 -
函数
-
单例内置对象:
Global和MathECMA-262中定义:任何由 ECMAScript 实现提供、与宿主环境无关,并在 ECMAScript 程序开始执行时就存在的对象
Global对象在浏览器中被实现为window对象。所有全局变量和函数都是Global对象的属性Math对象包含辅助完成复杂计算的属性和方法
-
-
集合引用类型
-
最常见的
Object和Array -
Map、WeakMap、Set以及WeakSet类型 -
定型数组:例如
Int32Array、Float64Array等等定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率。实际上, JavaScript 并没有“TypedArray”类型,它所指的其实是一种特殊的包含数值类型的数组。设计定型数组的目的就是提高与 WebGL 等原生库交换二进制数据的效率。
-
引用类型的值特点:可变、引用比较、值保存在堆(Heap)内存
类型判断
typeof
typeof 对判断原始值的类型简单有效。
typeof 37 === 'number';
typeof NaN === 'number';
typeof 42n === 'bigint';
typeof 'bla' === 'string';
typeof '1' === 'string';
typeof undefined === 'undefined';
typeof Symbol('foo') === 'symbol';
typeof false === 'boolean';
但是对引用值的作用不大,因为无论什么类型的对象(除了 Function 类型),使用typeof 都会返回 "object"。但是我们往往关心的不是这个值是不是对象,而是这个值是什么类型的对象。
typeof {a: 1} === 'object';
typeof [1, 2, 4] === 'object';
typeof new Date() === 'object';
typeof /regex/ === 'object';
除了正常的功能外,经常还会提起 typeof 的一些其它特性和例外:
null是简单类型的值,它有自己的类型Null,但是typeof null返回的却是object
在 JavaScript 最初的实现中,值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是
0。由于null代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签是0,typeof null也因此返回"object"。之后也是为了不得罪人,也就一直保留了这个问题特性。
-
判断函数返回的是
"function"typeof function() {} // "function" typeof class C {} // "function" typeof Math.sin // "function" typeof (() => {}) // "function"这里不好理解的是为什么
class C {}返回的也是"function"。其实 ECMAScript 中的class只不过是构造函数的语法糖而已。 -
括号?
typeof是操作符,语法上后面直接跟操作数,不需要括号。括号本身也是操作符,并且括号的优先级要高于typeof。所以当typeof后面跟上括号时,其实优先执行括号里的表达式并返回操作数,typeof判断的是这个返回的操作数的类型。typeof 42 + ' hello' // "number hello" typeof (42 + ' hello') // "string" (42 + ' hello') // "42 hello" -
暂时性死区导致的报错
-
在 ES6 之前,
typeof总能保证对任何所给的操作数返回一个字符串。使用typeof永远不会抛出错误。 -
ES6 新增了
let和const用来声明变量,使用它们会形成块级作用域,那么在变量声明之前对块中的let和const变量使用typeof会抛出一个ReferenceError(暂时性死区的特性)。typeof foo; let foo = 'bar'; // Uncaught ReferenceError: foo is not defined
-
-
一个“挑衅意味”的例外,对,就是各大浏览厂商故意这么做的(不过
document.all在HTML5 中已经被废弃,但是现在所有浏览器依然支持)console.log(document.all) // HTMLAllCollection(1713) [html,... 页面上的所有元素 typeof document.all // "undefined"
因为 typeof 无法很好的区分引用值是什么类型的对象,所以ECMAScript 提供了 instanceof 操作符来解决这个问题。
instanceof
如果变量是给定引用类型的实例,则 instanceof 操作符返回 true。instanceof 检测的是构造函数的 prototype (原型)是否存在于给定的对象实例的原型链上。
使用 instanceof 来判断原生对象类型,像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用:
var arr = [];
var arr1 = new Array(10);
console.log(arr instanceof Array); // true
console.log(arr1 instanceof Array); // true
ECMAScript 中可以使用自定义构造函数创建自定义类型的对象,那么依然可以使用 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
我们知道,每个对象实例都有一个 constructor 属性,指向用于创建当前对象的函数(constructor 属性是在构造函数的 prototype 上定义的)
Array.prototype.constructor === Array; // true
[].constructor === Array; // true
// 为什么原始值也有 constructor ? 可以看 typeof 那一节有解释
''.constructor === String // true
// null 和 undefined 不存在 constructor 的
constructor 本来是用于标识对象类型的。
function Person(name, age, job){
// ...
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
但是对于自定义构造函数一旦重写 prototype,可能会造成原有的 constructor 引用丢失,从而导致 constaructor 会变成默认的 Object:
function F() {}
// { foo: 'bar' } 是对象字面量,本身是一个 `Object` 实例,它自身的 constructor 指向 Object
F.prototype = { foo: 'bar' };
f.constructor === F
// false
f.constructor
// ƒ Object() { [native code] }
所以认为 instanceof 操作符是确定对象类型更可靠的方式。但是 instanceof 真的可靠吗?
并不可靠,就像上面说的,因为构造函数的 prototype 是可以改变的,所以在创建实例之后,修改构造函数的 prototype 属性,如果修改后的原型不在实例对象的原型链上了,那就会返回 false:
function F() {}
var f = new F;
F.prototype = {}
f instanceof F // false
还有一种方法是借助非标准的 __proto__ 伪属性来修改实例的原型链,使实例的原型链上不再出现创建它的构造函数的原型:
function F() {}
var f = new F;
f.__proto__ = {};
f instanceof F // false
使用 instanceof 还有一些其他问题需要注意:
-
不适用于原始值
var simpleStr = "This is a simple string"; var newStr = new String("String created with constructor"); simpleStr instanceof String; // 返回 false, 非对象实例,因此返回 false newStr instanceof String; // 返回 true -
Object.create(null) instanceof Object返回false。这是一种创建非Object实例的对象的方法 -
如果一个对象实例的原型链上不止一种构造函数的原型,那么使用
instanceof操作符检查这两种类型都会返回true
function foo() {}
foo instanceof Function // true
foo instanceof Object // true
上面的 Person 自定义构造函数创建的对象实例 person1 和 person2,既属于 Person 类型,又属于 Object 类型
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
-
多全局对象引起的问题
在浏览器中,我们的脚本可能需要在多个窗口之间进行交互。多个窗口意味着多个全局环境,不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数。
这可能会引发一些问题。例如:
[] instanceof window.frames[0].Array // false因为
Array.prototype !== window.frames[0].Array.prototype,意思就是window.frames[0].Array的原型不在前者[]的原型链上。 -
instanceof是操作符,所以要注意操作符的优先级,例如检测对象不是某个构造函数的实例时:if (!(mycar instanceof Car)) { // ... } // 而不是使用 if (!mycar instanceof Car),逻辑非的优先级高于 instanceof
Symbol.hasInstance(ES6)
上一节说到 instanceof 操作符可以用来确定一个对象实例的原型链上是否有原型。在 ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。
function Foo() {}
let f = new Foo();
// 使用 instanceof
console.log(f instanceof Foo); // true
// 使用 Symbol. hasInstance
console.log(Foo[Symbol.hasInstance](f)); // true
这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用。由于 instanceof 操作符会在原型链上寻找这个属性定义,因此可以在继承的类上通过静态方法重新定义这个函数,从而可以用它自定义 instanceof 操作符在某个类上的行为:
class Array1 {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof Array1); // true
Object.prototype.toString()
每个对象都有一个 toString() 方法,默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回一个字符串 "[object type]",其中 type 是对象的类型。
var o = new Object();
o.toString(); // [object Object]
为了获取准确的数据类型,我们最常用的方法也是目前最全最有效的方法就是使用 Object.prototype.toString() 来检测。需要注意的是:
-
不是直接使用
toString方法,而是使用Object构造函数原型上的方法,因为其它内置对象或者自定义类型的对象可能会实现自己的toString方法或者根本没有定义该方法,导致返回的不再是 “[object type]” 或者直接报错var str = new String('hello') str.toString() // "hello" 123..toString() // "123" null.toString() // Uncaught TypeError: Cannot read property 'toString' of null undefined.toString() // Uncaught TypeError: Cannot read property 'toString' of undefined -
需要以
Function.prototype.call()或者Function.prototype.apply()的形式来调用(不陌生吧,改变this指向)var toString = Object.prototype.toString; toString.call(new String); // "[object String]" toString.call('hello'); // "[object String]" toString.call(Math); // "[object Math]" toString.call(123); // "[object Number]" toString.call(true); // "[object Boolean]" toString.call(123n); // "[object BigInt]" toString.call(new Date); // "[object Date]" toString.call(Symbol()) // "[object Symbol]" toString.call(() => {}) // "[object Function]" toString.call([1, 2]); // "[object Array]" toString.call(new Map) // "[object Map]" toString.call(function* () {}); // "[object GeneratorFunction]" toString.call(Promise.resolve()); // "[object Promise]" // ES5 中才实现下面的功能 toString.call(undefined); // "[object Undefined]" toString.call(null); // "[object Null]"
这种方法对于日常开发中的大部分开发场景都适用了,但是对于开发者创建的类,默认情况下 toString() 只会返回默认的 Object 标签:
class ValidatorClass {}
Object.prototype.toString.call(new ValidatorClass()); // "[object Object]"
那有没有方法让自定义的类实例返回我们想要的结果呢?在 ES2015 标准中有一个内置 Symbol :Symbol.toStringTag。
Symbol.toStringTag(ES6)
Symbol.toStringTag 的值是一个字符串,这个字符串表示该对象的自定义类型标签。上面说到 Object.prototype.toString() 默认会返回返回一个字符串 "[object type]",其中的“type”有时候就是读取的 Symbol.toStringTag 的值。
不是所有的内置 JavaScript 对象类型都有 Symbol.toStringTag 属性,比如 ES6 之前使用 Object.prototype.toString() 也能返回类型:
Object.prototype.toString.call('foo'); // "[object String]"
Object.prototype.toString.call([1, 2]); // "[object Array]"
Object.prototype.toString.call(3); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
// ...
var o = new Object;
console.log(o[Symbol.toStringTag]); // undefined
JavaScript 引擎会为一些对象类型设置好 toStringTag 标签:
Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
// ...
var m = new Map;
console.log(Map.prototype[Symbol.toStringTag]); // 'Map'
console.log(m[Symbol.toStringTag]); // 'Map'
但是开发者自己创建的类需要手动添加 toStringTag 属性,否则引擎无法识别只能返回默认的 [object Object]
class ValidatorClass {
get [Symbol.toStringTag]() {
return "Validator";
}
}
Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]"
Symbol.toStringTag 的属性特性:是【不可写】、【不可枚举】并且【不可配置】
Map.prototype[Symbol.toStringTag]; // "Map"
Map.prototype[Symbol.toStringTag] = 'map';
var m = new Map;
m[Symbol.toStringTag]; // "Map"
m[Symbol.toStringTag] = 'map';
m[Symbol.toStringTag] // "Map"
Map.prototype[Symbol.toStringTag] // "Map"
// ---
var va = new ValidatorClass
va[Symbol.toStringTag] = 'va'
va[Symbol.toStringTag] // "Validator"
但是在测试的时候发现,内置的 JavaScript 对象类型,如果本身没有设置 Symbol.toStringTag 属性的话,我们是可以赋值并改写它的值的
Object.prototype[Symbol.toStringTag] = 'o'
var o = new Object
o[Symbol.toStringTag] // "o"
o[Symbol.toStringTag] = "object"
o[Symbol.toStringTag] // "object"
Object.prototype[Symbol.toStringTag] // "o"
Object.prototype.toString.call(o); // "[object object]"
目前不知原因。
ECMAScript 内置的针对某个类型的判断方法
-
Array.isArray()用于确定传递的值是否是一个ArrayArray.isArray([1]); // true Array.isArray(new Array('a', 'b', 'c', 'd')); // true有个非常细节的知识点,
Array.prototype是一个数组Array.isArray(Array.prototype); // true console.log(Array.prototype); // [constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]对于定型数组(TypedArray)实例,总是返回
falseArray.isArray(new Uint8Array(32)); // falseArray.isArray()能检测iframes,所以针对数据的检查,应该使用它而不要使用instanceof// 偷懒,直接搬 MDN 上的 var iframe = document.createElement('iframe'); document.body.appendChild(iframe); xArray = window.frames[window.frames.length-1].Array; var arr = new xArray(1,2,3); // [1,2,3] Array.isArray(arr); // true arr instanceof Array; // false -
isNaN()和Number.isNaN()用来确定一个值是否为NaN全局对象的
isNaN方法会将不是Number类型的参数转换为数值(空字符串和布尔值分别会被强制转换为数值 0 和 1),然后才会对转换后的结果是否是NaN进行判断。这就产生了有趣的现象isNaN(NaN); // true isNaN(undefined); // true isNaN({}); // true isNaN(true); // false isNaN(null); // false isNaN(37); // false // strings isNaN("37"); // false: 可以被转换成数值37 isNaN("37.37"); // false: 可以被转换成数值37.37 isNaN("37,5"); // true isNaN('123ABC'); // true: parseInt("123ABC")的结果是 123, 但是Number("123ABC")结果是 NaN isNaN(""); // false: 空字符串被转换成0 isNaN(" "); // false: 包含空格的字符串被转换成0 // dates isNaN(new Date()); // false isNaN(new Date().toString()); // true isNaN("blabla") // true: "blabla"不能转换成数值 // 转换成数值失败, 返回NaN所以建议使用
Number.isNaN()方法来确定传递的值是否为NaN,它是比原来的全局isNaN()的更安全的版本。它不会将Number.isNaN("NaN"); // false,字符串 "NaN" 不会被隐式转换成数字 NaN。 Number.isNaN(undefined); // false Number.isNaN({}); // false Number.isNaN("blabla"); // false
总结
- ECMAScript 标准定义了 8 种类型,包括7种基本数据类型和1种复杂(引用)类型
- 有明确期望的类型判断,可以使用内置方法,例如
Array.isArray()和isNaN - 对于原始值可以使用
typeof操作符,对于引用类型的值可以使用instanceof操作符确定对象类型。对于ES6的代码,可以通过重写类的Symbol.hasInstance静态方法来自定义instanceof的行为 - 更全更有效的检查方法
Object.prototype.toString(),调用的时候需要使用.call()或者.apply() - 自定义类型的实例对象如何通过
Object.prototype.toString()返回期望的类型字符串,可以通过添加Symbol.toStringTag属性。