前言
我们在前面几章中介绍了this的绑定,说明了this最常被搞混的观点也介绍了如何透过call-site与绑定的4个规则确定this所指向的物件是那一个,介绍了这么多this所指向的物件位置,那么物件到底是什么?而为什么我们的this会需要指向它呢?我们会在本篇章中进行讲解。
Syntax
物件有两种形式:declaration与constructed。
declaration:var myObj = {key: value// ...};
constructedvar myObj = new Object();myObj.key = value;
使用两种方法产生的物件完全相同,他们唯一的区别在于如果你使用declaration可以在物件中添加一个或多个key/value
,但是使用constructed只能将属性一个一个加入
。
Type
物件是构建大部分JS的通用模块,他是JS中6种主要类型之一。
stringnumberbooleannullundefinedobjectnull有时候会被当成一个物件类型,这种误解来自于JS中的一个bug使得typeof null
会回传object,但这是不对的,因为null有属于自己的类型,还有一个常见的错误判断JavaScript中的一切都是物件
,这是不对的。
除了上面的六种着要的型态之外,还有一些特殊的存在称为物件子类别(複杂基本类型)
,function是物件的一种子类型,function在JS中被称为first class
类型,因为他们基本上就是普通的物件而且可以被当作其他物件一样处理,而阵列也是一种形式的物件,他在内容的组织的结构化上会比一般物件好。
Built-in Objects
还有其他物件子类别通常称为内置物件
,它们的名称看起来和它们对应的基本类型有联繫,但事实上它们的关係更複杂。
但是在JS中这些都只是内建的函数,他们都可以通过new来创建出来,而创建出来的是一个新的子类型constructed物件。
var strPrimitive = "I am a string";typeof strPrimitive; // "string"strPrimitive instanceof String;// falsevar strObject = new String( "I am a string" );typeof strObject; // "object"strObject instanceof String; // true
基本类型的"I am a string"他是一个不可变的字串而不是物件,为了对这个字串进行操作(检查长度...)就会需要将这个字串变为物件形式,但是幸运的事JS对于这种情况,他会在需要的时候自动的
将"string"
强制转换为String
类型(auto-boxing),这意味着你不需要明确的创建这个字串的物件就可以对他进行操作。
var strPrimitive = "I am a string"; // type -> "string"// auto transform to String(object)console.log( strPrimitive.length );// 13console.log( strPrimitive.charAt( 3 ) ); // "m"
对于这种自动转换型别的也发生在number与boolean,但是null与undefined没有物件形式
,他们只有自己的基本类型,Date只能透过new建立所以他没有基本型态。
无论使用declaration还是constructed建立Objects
、Array
、Function
、RegExps
他们都是物件。
Error
很少明确且直接的被创建出来,通常在有异常的时候自动被创建并且掷出,可以由new Error(...)
建立出来不过很少见。
Contents
物件的内容会储存在物件中某些特定命名的位置上,我们称这些储存在物件中的值为properties
。
虽然我们说内容是存在于物件之中,但其实这只是一种看起来
而已,对JS来说他是以依赖(implementation-dependent)
的方式储存并且很有可能不将内容储存在物件容器中
,只有这些properties的名称储存在容器
,而这些properites名称会当作指向储存内容的位置的指针,换句话说储存在物件容器内的properties名称是物件内容的Reference
。
var myObject = {a: 2};myObject.a;// 2myObject["a"]; // 2
若要放问到myObject中的a需要使用.
或[]
运算符,.a
通常用于取得物件的property,而["a"]
用于键(key)的访问,实际上这两个用法访问到的位置是相同的所以都可以使用。
两种访问最主要的区别在于,如果使用.
则后面需要一个兼容标识符(Identifier)
的属性名称,而[".."]
中则可以接收任何兼容UTF-8/unicode的字串作为属性名,举个例子若你的物件中有个Super-Fun!
属性,就只能使用["Super-Fun!"]来访问这个属性,因为他不是一个合法的标识符(Identifier)。
由于["..."]
是使用字串
所以可以在程式中动态的变更我们需要访问的位置
var wantA = true;var myObject = {a: 2, b: 3};var idx;if (wantA) {idx = "a";}console.log( myObject[idx] ); // 2
由于物件的属性必须
得是字串,所以当你填入非字串的属性名则会优先将它转变为字串
,转换的範围甚是包夸number。
var myObject = { };myObject[true] = "foo";myObject[3] = "bar";myObject[myObject] = "baz";myObject["true"];// "foo"myObject["3"];// "bar"myObject["[object Object]"];// "baz"
Computed Property Names
使用["..."]
还可以对键(key)进行操作,比如说Object[prefix + name]。
var prefix = "foo";var myObject = {[prefix + "bar"]: "hello",[prefix + "baz"]: "world"};myObject["foobar"]; // hellomyObject["foobaz"]; // world
Property vs. Method
对于在物件中的function来说,许多开发者将他与property区分开来,我们称它为method,因为以技术上来说function他其实是不属于物件
,他在物件中只是以一个Reference的形式储存,所以当物件访问一个function的时候就很像是一个方法(method)
,虽然这是一个满牵强的理由XD。
Array
阵列也使用[]
来访问其中的元素,但是阵列在储存值以及储存位置的结构上更具有组织性,阵列採用数字索引
这意味着这个元素被储存的位置,必须是一个非负整数
。
var myArray = [ "foo", 42, "bar" ];myArray.length;// 3myArray[0];// "foo"myArray[2];// "bar"
在上面有提到其实阵列也是一种物件,所以你也可以对这个阵列增加属性
var myArray = [ "foo", 42, "bar" ];myArray.baz = "baz";myArray.length;// 3myArray.baz;// "baz"
虽然阵列可以达到与物件一样的效果(增加键/值)但是不推荐做这种操作,因为阵列本身有他的用途与使用方法,所以建议用物件来储存键/值而不适用阵列。
还有一个直得注意的地方,虽然我们对myArray
添加了属性,但是可以发现myArray.length
的长度并没有被改变,但是如果在阵列的属性中添加的值看起来像个数字,则他最终会变成阵列的索引
。
var myArray = [ "foo", 42, "bar" ];myArray["3"] = "baz";myArray.length;// 4myArray[3];// "baz"
除了会更改阵列长度之外,如果添加的数字属性名是已经存在于阵列中的index,则会改变其阵列的内容
var myArray = [ "foo", 42, "bar" ];myArray["1"] = "baz";myArray.length;// 3myArray[1];// "baz"
Duplicating Objects
在我们创建了一个物件时,可能会面临到需要複製物件的情况,一开始可能会觉得就单纯将这个物件複製过去(就跟一般的value一样),但是其实JS的物件複製比这个来的複杂多了,要介绍物件的複製首先要先区分浅拷贝
与深拷贝
的区别。
浅拷贝
对于複製物件来说,obj1 = obj2
他所传递的不是obj2的值而是obj2的Reference
,这意味着他们是共用同一个记忆体空间,所以当一个更改了另一个也会被影响而一同变更
。
var obj1 = { a: 10, b: 20, c: 30 };var obj2 = obj1;obj2.b = 100;console.log(obj1); // { a: 10, b: 100, c: 30 } <-- b 被改到了console.log(obj2); // { a: 10, b: 100, c: 30 }
(图片来源 : [Javascript] 关于 JS 中的浅拷贝和深拷贝)
深拷贝
深拷贝与浅拷贝的只複製Reference不同,他会创造
一个新的物件,新物件与旧物件不会共用一个记忆体空间,所以修改新物件不会同步影响到旧物件。
var obj1 = { a: 10, b: 20, c: 30 };var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };obj2.b = 100;console.log(obj1); // { a: 10, b: 20, c: 30 } <-- b 没被改到console.log(obj2); // { a: 10, b: 100, c: 30 }
(图片来源 : [Javascript] 关于 JS 中的浅拷贝和深拷贝)
介绍完什么是浅拷贝与深拷贝后,我们回到本书
function anotherFunction() { /*..*/ }var anotherObject = {c: true};var anotherArray = [];var myObject = {a: 2,b: anotherObject,// reference, not a copy!c: anotherArray,// another reference!d: anotherFunction};anotherArray.push( anotherObject, myObject );
将过介绍什么是深浅拷贝后,可以发现myObject
中的b、c、d都不是物件的複製而只是共用了相同的Reference(浅拷贝),如果要将b、c、d深拷贝给myObject,有一个解决方法就是使用JSON,将物件使用JSON.stringify
转变为字串后再用JSON.parse
转回物件,这样就可以得到一个深拷贝。
var obj1 = { body: { a: 10 } };var obj2 = JSON.parse(JSON.stringify(obj1));obj2.body.a = 20;console.log(obj1); // { body: { a: 10 } } <-- 没被改到console.log(obj2); // { body: { a: 20 } }console.log(obj1 === obj2); // falseconsole.log(obj1.body === obj2.body); // false
还有另一种深拷贝的方法,ES6提供了一个新函数Object.assign
。
var obj1 = { a: 10, b: 20, c: 30 };var obj2 = Object.assign({}, obj1);obj2.b = 100;console.log(obj1); // { a: 10, b: 20, c: 30 } <-- 没被改到console.log(obj2); // { a: 10, b: 100, c: 30 }
Object.assign({},obj1)
的第一个参数{}代表他会建立一个空的物件,接着再把obj1中的properties複製过去,所以obj2会长得跟obj1一样但是却不是共用同一个记忆体位置,不过要注意的是Object.assign只能複製一层
的物件。
除了使用ES6提供的Object.assign
之外,也可以使用ES6提供的...(展开运算子spread operator)
将obj1的物件複製到空物件中
var obj1 = { a: 10, b: 20, c: 30 };var obj2 = { ...obj1, // 展开obj1并複製 b: 100, // 更改obj2中的值}console.log(obj1); // { a: 10, b: 20, c: 30 } <-- 没被改到console.log(obj2); // { a: 10, b: 100, c: 30 }
结论
在本章节中我们介绍了物件是什么、型态与一些特性,让我们来整理一下
物件可以透过declaration与constructed建立出来,他们的结果是一样的,唯一的差别是declaration建立的物件可以一次性的加入一个或多个数性
,而constructed只能一个一个加入
。物件的型态String:随着需要而将原始型态转变为物件。Number:随着需要而将原始型态转变为物件。Boolean:随着需要而将原始型态转变为物件。Object:无论使用declaration或constructed建立,都是物件。Function:无论使用declaration或constructed建立,都是物件。Array:无论使用declaration或constructed建立,都是物件。Date:只能通过constructed建立。RegExp:无论使用declaration或constructed建立,都是物件。Error:可以通过constructed建立,但不常见。可以使用.
与["..."]
访问到物件的properties,他们的区别在于.
需要符合Identifier而["..."]
只要是UTF-8/unicode的字串都可以。物件中的值称为property
,函数称为method
。阵列也是物件,所以也可以对阵列加入属性(不建议)。对阵列加入属性,如果属性名称是数字则会改变阵列index的情况
。物件的複製有分深拷贝
与浅拷贝
,浅拷贝只是複製物件的Reference所以是共用同一个记忆体位置; 深拷贝是创造一个新的物件。参考文献:
You Don't Know JavaScript
[Javascript] 关于 JS 中的浅拷贝和深拷贝