数据类型
总共 7 种
ECMAScript 有 6
种简单数据类型(也称为原始类型):Undefined
、Null
、Boolean
、Number
、String
和Symbol
;还有一种复杂数据类型:Object
。
除过上面的 6 种基本数据类型外,剩下的就是引用数据类型了,统称为 Object
类型。细分的话,有:Object
类型、Array
类型、Date
类型、RegExp
类型、Function
类型 等。
基本数据类型
下面记录三个需要注意的数据类型:
一、Undefined 类型Undefined
类型只有一个值,就是 undefined
。当使用 var
或 let
声明了变量但没有初始化时,就相当于给变量赋予了 undefined
值,因此:
let message;
console.log(message == undefined); // true
ECMA-262 第3版之前,字面量 undefined
是不存在的,增加这个值是为了正式明确空对象指针(null)和未初始化变量的区别。
需要注意的是,未声明的变量和声明了但未赋值的变量是不同的,请看下面例子:
let message;
console.log(message); // "undefined"
console.log(msg); // 报错
前面说过,声明变量时没有进行初始化就相当于给变量赋予 undefined
值,于是可以打印出 "undefined"
;但未声明的变量进行打印等操作就会报错,这点不难理解。难以理解的是下面这一点,对于未声明的变量,只能执行一个有效的操作,那就是对它调用 typeof
(对未声明的变量调用 delete
也不会报错,只是没什么用,实际上在严格模式下会抛出错误),问题来了:在对未初始化的变量调用 typeof
时,返回的结果是 "undefined"
,但对为声明的变量调用它时,也返回了 "undefined"
,这就有点不是很好。比如:
let message;
console.log(typeof message); // "undefined"
console.log(typeof msg); // "undefined"
无论变量是否声明,typeof
返回的都是 "undefined"
。逻辑上讲这是对的,因为虽然严格来讲这两个变量存在根本性差异,但它们都无法执行实际操作。
!注意:建议声明变量的同时进行初始化。这样,当
typeof
返回"undefined"
时,开发者就能知道那是因为给定的变量尚未声明,而不是声明了但未初始化。
二、NullNull
类型只有一个值,即特殊值 null
。**null
值表示一个空对象指针**,这就是 typeof null
会返回 "object"
的原因:
let message = null;
console.log(message); // "object"
!建议:在定义将来要保存对象值的变量时,建议使用 null 来初始化,而不是使用其他值。这样,只要检查整改变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用,比如:
let message = null; if (message != null) { // message 是一个对象的引用 }
这里还有一个小点: undefined
值是由 null
值派生来的,因此 ECMA-262 将它们定义为表面上相等,如 console.log(null == undefined); // true
三、Symbol
具体的参考阮一峰老师的 ECMAScript 6 入门之 Symbol,这里就不班门弄斧了。
基本数据类型的特性
- 基本数据类型的值是不可变的,任何方法都无法改变一个基本类型的值,比如一个字符串:
var name = "change"; name.substr(1); // hang console.log(name); // change var s = "hello"; s.toUpperCase(); // HELLO console.log(s); // hello
- 通过这两个例子,我们会发现原先定义的变量name的值始终没有发生改变。
- 而调用
substr()
和toUpperCase()
方法后返回的是一个新的字符串,跟原先定义的变量 name 并没有关系。
或许有人会有以下的疑问:
var name = "change";
name = "change1";
console.log(name); // change1
- 这样看起来 name 的值“改变了”,其实 var name = “change”,这里的基础类型是 string ,也就是“change”,这里的“change”是不可以改变的,name只是指向“change”的一个指针,指针的指向可以改变,所以你可以name = “change1”,代表此时name指向了“change1”。同理,这里的“change1”同样不可以改变。
- 也就是说这里你认为的改变只是“指针的指向改变”,这里的基础类型指的是“change”,而不是name,需区分清楚。
- 基本数据类型不可以添加属性和方法
通过上面的代码,我们知道不能给基本类型添加属性和方法,也再次说明基本类型是不可变的。var p = "change"; p.age = 29; p.method = function(){console.log(name)}; console.log(p.age); // undefined console.log(p.method); // undefined
- 基本数据类型的赋值是简单赋值
如果从一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上
var a = 10;
var b = a;
a++;
console.log(a); // 11
console.log(b); // 10
上面的代码中, a 中保存的值是10。当使用 a 的值来初始化 b 时, b 中也保存了值 10 。但 b 中的 10 和 a 中的 10 是完全独立的。 b 中的值只是 a 中值的一个副本。所以这两个变量可以参与任何操作而不会相互影响。
4. 基本数据类型的比较是值的比较
var a = 1;
var b = true;
console.log(a == b); // true
console.log(a == b); // false
上面 a 和 b 的数据类型不同,但是也可以进行值的比较,这是因为在比较之前,自动进行了数据类型的 隐式转换。
5. 基本数据类型是存放在栈区的
假如有以下几个基本类型的变量:
var name = "jozo";
var city = "guangzhou";
var age = 22;
那么它的存储结构如下所示:
|栈区A|栈区A|
|:–:|:–:|
|name|jozo|
|city|guangzhou|
|age|22|
同一个栈区里包括了变量的标识符和变量的值
引用数据类型
引用数据类型的特性
- 引用类型的值是可以改变的
- 引用类型可以添加属性和方法
- 引用类型是同时保存在栈区和堆区中的
引用类型的存储需要在内存的栈区和堆区共同完成,栈区保存变量标识符和指向堆内存的地址
假如有以下两个对象:
let man = {}
let woman = {}
console.log(man === woman) // 输出:false
则这两个对象在内存中保存的情况如下图:
4. 引用类型的赋值是对象引用
var a = {};
var b = a;
a.name = "change";
console.log(a.name); // change
console.log(b.name); // change
b.age = 29;
console.log(a.age); // 29
console.log(b.age); // 29
当 a 向 b 赋值引用类型的值时,同样也会将储存在 a 中的对象的值复制一份,并放到为 b 分配的空间中。此时引用类型保存在 b 中的是对象在堆内存中的地址。所以,与基本数据类型的简单赋值不同,这个值的副本实际上是一个指针,而这个指针指向存储在堆内存的一个对象。那么赋值操作后,两个变量都保存了同一个对象地址,而这两个地址指向了同一个对象。因此,改变其中任何一个变量,都会互相影响。 他们的关系如下图:
5. 引用类型的比较是引用的比较
var person1 = {};
var person2 = {};
console.log(person1 == person2); // false
- Q:为什么两个对象看起来一摸一样,但是却不相等呢?
- A:因为引用类型的比较是引用的比较,换句话说,就是比较两个对象保存在栈区的指向堆内存的地址是否相同,此时,虽然 p1 和 p2 看起来都是一个”{}”,但是他们保存在栈区中的指向堆内存的地址却是不同的,所以两个对象不相等
判断数据类型的方法
typeof 操作符
用于确定任意变量的数据类型,对一个值使用typeof
操作符会返回下列字符串之一: - “undefined” 表示值未定义;
- “boolean” 表示值为布尔值;
- “string” 表示值为字符串;
- “number” 表示值为数值;
- “object” 表示值为对象(而不是函数)或 null;
- “function” 表示值为函数;
- “symbol” 表示值为符号;
使用 typeof 操作符的示例:
// es5
typeof '' // string
typeof 1 // number
typeof true // boolean
typeof undefined // undefined
typeof Array // function
typeof {} // object
typeof [] // object
typeof Symbol() // symbol
typeof console // object
typeof console.log // function
typeof null // object
需要注意的是:
null
会返回"object"
是因为特殊值null
被认为是一个对空对象的引用(这只是JavaScript
存在的一个悠久 Bug,不代表null
就是引用数据类型,并且null
本身也不是对象)。那为什么会被这么认为呢?这就需要了解变量是如何被存储的。在Javascript
底层存储变量的时候,会在变量的机器码低位 1-3 位表示类型信息:
- 000:对象
- 010:浮点数
- 100:字符串
- 110:布尔
- 1:整数
- null:所有码都是0
- undefined:用 -2^30 表示
而
null
的低位 1-3 解析到的为 000,刚好与object
一样,因此就被当做了对象来看待。
typeof
的实现大致如下,在 JS 诞生之初就只有六种类型判断:
if (JSVAL_IS_VOID(v)) { // 判断是否为 undefined
type = JSTYPE_VOID;
} else if (JSVAL_IS_OBJECT(v)) { // 判断是否为 object
obj = JSVAL_TO_OBJECT(v);
if (obj && (ops = obj->map->ops, ops == &js_ObjectOps)
? (clasp = OBJ_GET_CLASS(cx, obj), clasp->call || clasp == &js_FunctionClass)
: ops->call != 0) {
type = JSTYPE_FUNCTION;
} else {
type = JSTYPE_OBJECT;
}
} else if (JSVAL_IS_NUMBER(v)) { // 判断是否为 number
type = JSTYPE_NUMBER;
} else if (JSVAL_IS_STRING(v)) { // 判断是否为 string
type = JSTYPE_STRING;
} else if (JSVAL_IS_BOOLEAN(v)) { // 判断是否为 boolean
type = JSTYPE_BOOLEAN;
}
instanceof 操作符
使用方法:
// 判断一个实例是否属于某种类型:
let person = function () {};
let nicole = new person();
nicole instanceof person; // true
// 判断一个实例是否是其父类型或者祖先类型的实例
let person = function () {};
let programmer = function () {};
programmer.prototype = new person();
let nicole = new programmer();
nicole instanceof person // true
nicole instanceof programmer // true
用法挺简单的,那么其底层实现原理是什么呢?根据 ECMAScript 语言规范,大概的思路如下:
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
// 循环体一直循环,直到在 leftVaule 的原型链上找到 rightVaule;或者走到 leftVaule 原型链的尽头也还是找不到 rightVaule ,则退出循环
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
// 不用执行 rightVaule = rightVaule.__proto__ ,因为 rightVaule 就是要查找的对象
leftVaule = leftVaule.__proto__ ;
}
}
总而言之,instanceof
主要的实现原理就是只要右边变量的 prototype
在左边变量的原型链上即可。因此,要想理解 instanceof
的原理,就还必须熟悉 JavaScript 的原型继承原理,不太清楚的小伙伴可以浏览我的 另一篇文章。