JavaScript原始类型和引用类型

引子

理解和使用对象是理解整个 JavaScript 的关键

JavaScript 把对象作为语言的中心,几乎所有的 JavaScript 的数据要么是一个对象要么是从对象中获取。我们可以在任何时候创建对象,在任何时候给对象添加/删除属性、方法。事实上,函数在 JavaScript 中也被视为对象。

JavaScript 中的数据分为两种类型:原始类型和引用类型,两者都通过对象进行访问。原始类型保存为简单数据值,引用类型则保存为对象,其本质是指向内存位置的引用。JavaScript 使用一个变量对象追踪变量的生存周期。原始类型的变量值被直接保存在变量对象内,而引用类型的变量值则作为一个指针保存在变量对象内,该指针指向实际对象在内存中的存储位置。


原始类型

原始类型共有以下五种

boolean    //布尔,值为 true 或 false  
number     //数字,值为任何整型或浮点型数值
string     //字符串,值为由单引号或双引号包围的单个字符或连续字符(JavaScript 不区分字符类型)
null       //空类型,该原始类型只有一个值:null
undefined  //未定义,该原始类型只有一个值:undefined

关于未定义类型,有一点需要注意,未经初始化的变量的值在 JavaScript 中是 undefined,如下:

var ref;   //变量 ref 的值此时是 undefined

鉴别原始类型

鉴别原始类型的最佳方法是使用 typeof 操作符。它可以被用在任何变量上,并返回一个说明数据类型的字符串。

console.log(typeof 'a');           // "string"
console.log(typeof "Nicholas");    // "string"
console.log(typeof 10);            // "number"
console.log(typeof 5.1);           // "number"
console.log(typeof false);         // "boolean"
console.log(typeof true);          // "boolean"
console.log(typeof undefined);     // "undefined"

上面这段代码不仅展现了 typeof 操作符的用法,也说明了 JavaScript 中不同的数据各是属于什么类型。正如我们所期望的,对于单个字符和字符串,typeof 将返回 “string”,对于数字将返回 “number” (无论是整型还是浮点型),对于布尔类型将返回 “boolean”,对于未定义类型则将返回 “undefined”。

然而,空类型却是一个意外。

console.log(typeof null);          // "object"

值为 null,根据前面原始类型的值来看,其应该是属于 null 类型的啊,然而这里 typeof 操作符的结果却让我们感到意外。这是为什么呢?其实,这已经被设计和维护 JavaScript 的委员会 TC39 认定是一个错误。在逻辑上,我们可以认为 null 是一个空的对象指针,所以结果为 “object”。


判断空类型

由于typeof null //"object",所以我们不能使用 typeof 操作符来判断一个变量是否为空类型。判断是否为空类型的最佳方法是直接和 null 比较,如下例:

//value 为要判断是否为空类型的变量
console.log(value === null);       // true 或 false

这里之所以不使用双等号操作符(==)是因为双等号操作符在进行比较时会默认把不同类型的值转换成相同类型再进行比较。

console.log("5" == 5);             // true
console.log("5" === 5);            // false
console.log(undefinded == null);   // true
console.log(undefined === null);   // false

上面这段代码展示了双等号与三等号的区别:双等号仅仅比较符号两侧变量的值,如果二者的类型不同,便会默认转换成相同类型再进行值的比较;三等号不仅比较符号两侧变量的值,而且也会比较二者的类型,只有当符号两侧变量的值和类型均相等时,才会认定符号两侧变量相等。


引用类型

引用类型是指 JavaScript 中对象,引用值是引用类型的实例,也是对象的同义词。对象实际上是属性的无序列表。属性包含键(始终是字符串)和值。如果一个属性的值是函数,它就被称为方法。JavaScript 中函数其实是引用值,一个包含数组的属性和一个包含函数的属性没有什么区别。

鉴别引用类型

函数是最容易鉴别的引用类型,因为对函数使用 typeof 操作符时,返回值是“function”。

function reflect(value) {
    return value;
}

console.log(typeof reflect);    // "function"

然而对其他引用类型的鉴别使用 typeof 操作符却没什么效果,因为对于所有非函数的引用类型,typeof 返回“object”,而这没什么实际价值。为了更方便地鉴别引用类型,我们使用 instanceof 操作符。instanceof 操作符以一个对象和一个构造函数为参数,如果对象是构造函数所指定的类型的一个实例,instanceof 返回 true;否则返回 false。例如:

var items = [];
var object = {};

function reflect(value) {
    return value;
}

console.log(items instanceof Array);       // true
console.log(object instanceof Object);     // true
console.log(reflect instanceof Function);  // true

myobj instanceof constructor本质上是判断对象 myobj 是 constructor.prototypr 的一个实例吗?即在对象 myobj 的原型链上寻找是否有 constructor。所以,不难理解,instanceof 操作符还可以用来鉴别继承类型。JavaScript 中所有引用类型都继承自 Object 类型,使用 instanceof 来检测如下例所示:

var items = [];
var object = {};

function reflect(value) {
    return value;
}

console.log(items instanceof Object);    // true
console.log(object instanceof Object);   // true
console.log(reflect instanceof Object);  // true

鉴别数组

使用 instanceof 可以检测数组,但是有一个例外会影响网页开发者:JavaScript 的值可以在同一个网页的不同框架之间传来传去。当我们试图鉴别一个引用值的类型时,这就有可能成为一个问题,因为每一个页面拥有它自己的全局上下文——Object、Array 以及其他内建类型的版本。结果,当我们把一个数组从一个框架传到另一个框架时,instanceof 就无法识别它,因为那个数组是来自不同框架的 Array 实例。

为了解决这个问题,ECMAScript 5 引入了 Array.isArray() 来明确鉴别一个值是否为 Array 的实例,无论该值来自哪里,该方法对来自任何上下文的数组都返回 true。例如:

var items = [];

console.log(Arrray.isArray(items));  // true

大多数环境都在浏览器和Node.js中支持 Array.isArray() 方法。IE8 或更早版本不支持这种方法。


对象

(1) 创建对象

要使用引用类型,必须先创建引用类型的实例——对象,创建对象亦可以称为实例化对象。创建对象有几种方式,但最常见的便是使用 new 操作符和构造函数,下面的代码演示了这种方式。事实上,用于配合 new 操作符创建对象的构造函数与普通函数并没有什么不同,任何函数都可以用作构造函数。为了与普通用途的函数区分开来,在命名规范上,JavaScript 中的构造函数首字母通常大写。

var obj1 = new Object();

上面这段代码实例化了一个通用对象,并将它的引用保存在变量 obj1 中。因为引用类型不在变量中直接保存对象,所以上面代码中的变量 obj1 实际上并不包含对象的实例,而是一个指向内存中对象所在位置的指针(或者说是引用)。这是对象和原始类型变量值之间的额一个基本区别,原始类型值是直接保存在变量中的。

当将一个对象赋值给变量时,实际是赋值给这个变量一个指针。这意味着,将一个变量赋值给另一个变量时,两个变量各获得了一份指针的拷贝,指向内存中的同一个对象。例如:

var obj1 = new Object();
var obj2 = obj1;

这段代码先用 new 创建了一个对象并将其引用保存在变量 obj1 中。然后将 obj1 的值赋给 obj2,于是两个变量都指向第一行被创建的那个对象的实例,如果其中一个保存引用的变量改变了对象的某些属性,那么改变会反映到内存中的对象本身。

(2) 对象引用解除

JavaScript 有垃圾收集的功能,因此当使用引用类型时无须担心内存分配。但最好在不使用对象时将其引用解除,让垃圾收集器对那块内存进行释放。解除引用的最佳手段是将对象变量重置为 null。

var obj1 = new Object();
//做一些操作
obj1 = null;

这里,对象 obj1 被创建然后使用,最后设置为 null。当内存中的对象不再被引用后,垃圾收集器会把那块内存挪作它用(在那些使用几百万对象的巨型程序里,对象引用解除尤其重要)。

(3) 添加删除属性

在 JavaScript 中,我们随时可以给对象添加(或删除)属性。例如:

var obj1 = new Object();
var obj2 = obj1;

obj1.myCustomProperty = "Awesome!";
console.log(obj2.myCustomProperty);    // "Awesome!"

这里,obj1 上增加了 myCustomProperty 属性,值为”Awesome!”。该属性也可以被 obj2 访问,因为 obj1 和 obj2 指向同一个对象。本例演示了 JavaScript 的一个独特的方面:可以随时修改对象,即使并没有在开始时定义它们。

(4) 访问属性

属性是对象中保存的名字和值的配对。点号是 JavaScript 中访问属性的最通用做法,不过也可以用中括号访问 JavaScript 对象的属性。

//使用点号访问
var array = [];
array.push(12345);

//使用中括号访问
var array = [];
array["push"](12345);

在需要动态决定要访问哪个属性时,中括号访问的语法特别有用。例如下例的中括号允许你用变量而不是字符串字面形式来指定访问的属性。

var array = [];
var method = "push";
array[method](12345);

在上面这段代码中变量 method 的值是“push”,因此在 array 上调用了 push() 方法。这种能力及其有用。记住一点:除了语法不同,在性能上或其他方面点号和中括号都大致相同,唯一区别在于中括号允许在属性名字上使用特殊字符。


内建类型

JavaScript 中常用的内建类型如下:

Array数组类型,以数字为索引的一组值的有序列表
Date日期和时间类型
Error运行错误类型(还有一些更特别的错误的子类型)
Function函数类型
Object通用对象类型
RegExp正则表达式类型

字面形式

内建引用类型有字面形式。字面形式允许在不需要使用 new 操作符和构造函数显示创建对象的情况下生成引用值。

(1) 对象字面形式

var book = {
    name : "The Principles of Object-Oriented JavaScript",
    var : 2014
};

使用对象字面形式创建对象,可以在大括号内定义一个新对象及其属性。属性的组成包括一个标识符、一个冒号以及一个值。多个属性之间用逗号分隔。注意大括号结尾有分号,因为这是一条 JavaScript 语句。属性的名字也可以用字符串表示,特别是当希望名字中包含空格或其他特殊字符时。例如:

var book = {
    "name" : "The Principles of Object-Oriented JavaScript",
    "year" : 2014
};

//以上代码等价于下面代码

var book = new Object();
book.name = "The Principles of Object-Oriented JavaScript";
book.year = 2014;

上述 3 例的结果是一致的,只是写法不同,上述3段代码均互相等价。虽然使用字面形式并没有调用 new Object(),但是 JavaScript 引擎背后做的工作和 new Object() 一样,除了没有调用构造函数。其他引用类型的字面形式也是如此。

(2) 数组字面形式

定义数组字面形式是在中括号内使用逗号区分的任意数量的值,例如:

var colors = [ "red", "blue", "green" ];

//以上代码等价于下面代码

var colors = new Array("red", "blue", "green");

需要注意的是,使用数组字面形式创建一个空数组的方式 var myArray = [] 此处是创建了一个没有任何元素的空数组。

(3) 函数字面形式

基本上都要用字面形式来定义函数。考虑到在可维护性、易读性和调试上的巨大挑战,通常不会有人使用函数的构造函数,因此很少看到用字符串表示的代码而不是实际的代码。使用字面形式创建函数更方便也更不容易出错,如下例:

function reflect(value) {
    return value;
}

//以上代码等价于下面代码

var reflect = new Function("value","return value;");

这段代码定义了 reflect() 函数,它的作用是将任何传给它的参数返回。即使是这样一个简单的例子,使用字面形式都比构造函数的形式方便和易读。另外,用构造函数创建的函数没什么好的调试方法;JavaScript 调试器认不出这些函数,它们在程序里就好像黑盒一样。

(4) 正则表达式字面形式

JavaScript 允许使用字面形式而不是使用 RegExp 构造函数定义正则表达式。模式被包含在两个“/”之间,第二个“/”后是由单字符表示的额外选项。例如:

var numbers = /\d+/g;    //正则表达式“\d+”由两个“/”包围,额外选项 g 表示此表达式起全局作用

//以上代码等价于下面代码

var numbers = new RegExp("\\d+","g");

使用字面形式比较方便的一个原因是不需要担心字符串中的转义字符。如果使用 RegExp 构造函数,传入模式的参数是一个字符串,你需要对任何反斜杠进行转义(这就是为什么字面形式使用“\d”而构造函数使用“\\d+”的原因)。在 JavaScript 中,除非需要通过一个或多个字符串动态构造正则表达式,否则都建议使用字面形式而不是构造函数。


原始包装类型

说明:原书译者译的是“原始封装类型”,但根据鄙人拙见,以及参考其他技术书籍,个人还是认为使用“原始包装类型”更符合 JavaScript 这个语法本身的含义。所以在个人笔记中使用的是“原始包装类型”这个概念。

原始封装类型一共有 3 种(String、Number 和 Boolean),这些特殊的引用类型存在的价值就是使得原始类型变量能够像引用类型值(即对象)一样方便使用,它们的存在使得原始类型变量可以调用方法来完成一些常见的操作。当读取字符串、数字或布尔值时,原始包装类型被自动创建。看下例代码:

var name = "Nicholas";
var firstChar = name.charAt(0);
console.log(firstChar);          // "N"

上面这段代码第一行,一个原始字符串的值被赋给 name。第二行代码把 name 当成一个对象,使用点号嗲用了 charAt() 方法。我们知道,原始类型是直接保存为简单数据值的,不允许也不可能有属于自己的属性和方法,但是这里我们给 name 调用了一个方法,而JavaScript 引擎却没有报错。这是为什么呢?这就是 JavaScript 中原始包装类型的功劳。原始类型值保存的是简单数据值,不能有属于自己的属性和方法毫无疑问是正确的;事实上,在此处,JavaScript 中的原始包装类型在幕后帮我们完成了一系列工作,以使得我们能够使用原始类型调用库方法。下面的代码向我们展现了幕后发生的事儿:

var name = "Nicholas";
var temp = new String(name);
var firstChar = temp.charAt(0);
temp = null;
console.log(firsrChar);          // "N"

由于源代码第二行把字符串当成对象使用,JavaScript 引擎创建了一个字符串的实例让 charAt() 可以工作。字符串对象的存在仅用于该语句并在随后被销毁(一种称为自动打包的过程)。为了测试这一点,试着给字符串添加一个属性看看它是不是对象。

var name = "Nicholas";
name.last = "Zakas";
console.log(last);               // undefined

这段代码试图给字符串 name 添加 last 属性。代码运行时没有错误,但是属性却消失了。到底发生了什么?我们可以在任何时候给一个真的对象添加属性,属性会保留至我们手动删除它们。原始包装类型的属性会消失是因为被添加属性的对象在使用它们的语句结束时就立刻被销毁了。下面是在 JavaScript 引擎中实际发生的事情:

var name = "Nicholas";
var temp = new String(name);
temp.last = "Zakas";
temp = null;                     //临时对象 temp 被销毁

var temp = new String(name);
console.log(temp.last);          // undefined
temp = null;

实际上是在一个立刻就被销毁的临时对象上而不是字符串上添加了新的属性。之后当我们试图访问该属性时,另一个不同的临时对象被创建,而新属性并不存在。虽然原始包装类型会被自动创建,但在这些值上进行 instanceof 检查对应类型的返回值却都是 false。

var name = "Nicholas";
var count = 10;
var found = false;

console.log(name instanceof String);        // false
console.log(count instanceof Number);       // false
console.log(found instanceof Boolean);      // false

这是因为临时对象仅在值被读取时创建。instanceof 操作符并没有真的读取任何东西,也就没有临时对象的创建,于是它告诉我们这些值并不属于原始包装类型。

我们也可以手动创建原始包装类型,但有某些副作用。

var name = new String("Nicholas");
var count = new Number(10);
var found = new Boolean(false);

console.log(typeof name);                   // "object"
console.log(typeof count);                  // "object"
console.log(typeof found);                  // "object"

如上例代码所示,手动创建原始包装类型会创建出一个 object,这意味着 typeof 操作符无法鉴别出你实际保存的数据的类型。

另外,使用 String、Number、Boolean 对象和使用原始值有一定区别。例如,下列代码使用了 Boolean 对象,对象的值是 false,但console.log("Found")依然会被执行。这是因为一个对象在条件判断语句中总被认为是 true,无论该对象的值是不是等于 false。

var found = new Boolean(false);
if (found) {
    console.log("Found");                   // true
}

手工创建的原始包装类型在其他方面也很容易让人误解,在大多数情况下都只会导致错误。所以,除非有特殊情况,我们应该避免这么做。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值