玩命加载中 . . .

JS 之数据类型


数据类型

总共 7 种

ECMAScript 有 6简单数据类型(也称为原始类型):UndefinedNullBooleanNumberStringSymbol;还有一种复杂数据类型:Object

除过上面的 6 种基本数据类型外,剩下的就是引用数据类型了,统称为 Object 类型。细分的话,有:Object类型、Array 类型、Date 类型、RegExp类型、Function 类型 等。

基本数据类型

下面记录三个需要注意的数据类型:
一、Undefined 类型
Undefined 类型只有一个值,就是 undefined。当使用 varlet 声明了变量但没有初始化时,就相当于给变量赋予了 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" 时,开发者就能知道那是因为给定的变量尚未声明,而不是声明了但未初始化。

二、Null
Null 类型只有一个值,即特殊值 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,这里就不班门弄斧了。

基本数据类型的特性

  1. 基本数据类型的值是不可变的,任何方法都无法改变一个基本类型的值,比如一个字符串:
    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,需区分清楚。
  1. 基本数据类型不可以添加属性和方法
    var p = "change";
    p.age = 29;
    p.method = function(){console.log(name)};
    console.log(p.age); // undefined
    console.log(p.method); // undefined
    通过上面的代码,我们知道不能给基本类型添加属性和方法,也再次说明基本类型是不可变的。
  2. 基本数据类型的赋值是简单赋值

如果从一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上

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|

同一个栈区里包括了变量的标识符和变量的值

引用数据类型

引用数据类型的特性

  1. 引用类型的值是可以改变的
  2. 引用类型可以添加属性和方法
  3. 引用类型是同时保存在栈区和堆区中的

引用类型的存储需要在内存的栈区和堆区共同完成,栈区保存变量标识符和指向堆内存的地址

假如有以下两个对象:

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 的原型继承原理,不太清楚的小伙伴可以浏览我的 另一篇文章


文章作者: hcyety
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hcyety !
评论
  目录