[JS] You Don't Know JavaScript [this & Object Prototypes] - Object [上]

前言

我们在前面几章中介绍了this的绑定,说明了this最常被搞混的观点也介绍了如何透过call-site与绑定的4个规则确定this所指向的物件是那一个,介绍了这麽多this所指向的物件位置,那麽物件到底是什麽?而为什麽我们的this会需要指向它呢?我们会在本篇章中进行讲解。

Syntax

物件有两种形式:declaration与constructed。

  • declaration:
var myObj = {
	key: value
	// ...
};
  • constructed
var myObj = new Object();
myObj.key = value;

使用两种方法产生的物件完全相同,他们唯一的区别在於如果你使用declaration可以在物件中添加一个或多个key/value,但是使用constructed只能将属性一个一个加入


Type

物件是构建大部分JS的通用模块,他是JS中6种主要类型之一。

  • string
  • number
  • boolean
  • null
  • undefined
  • object

null有时候会被当成一个物件类型,这种误解来自於JS中的一个bug使得typeof null会回传object,但这是不对的,因为null有属於自己的类型,还有一个常见的错误判断JavaScript中的一切都是物件,这是不对的。

除了上面的六种着要的型态之外,还有一些特殊的存在称为物件子类别(复杂基本类型),function是物件的一种子类型,function在JS中被称为first class类型,因为他们基本上就是普通的物件而且可以被当作其他物件一样处理,而阵列也是一种形式的物件,他在内容的组织的结构化上会比一般物件好。

Built-in Objects

还有其他物件子类别通常称为内置物件,它们的名称看起来和它们对应的基本类型有联系,但事实上它们的关系更复杂。

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
    但是在JS中这些都只是内建的函数,他们都可以通过new来创建出来,而创建出来的是一个新的子类型constructed物件。
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String;	// false

var 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 );	// 13
console.log( strPrimitive.charAt( 3 ) ); // "m"

对於这种自动转换型别的也发生在number与boolean,但是null与undefined没有物件形式,他们只有自己的基本类型,Date只能透过new建立所以他没有基本型态。

无论使用declaration还是constructed建立ObjectsArrayFunctionRegExps他们都是物件。

Error很少明确且直接的被创建出来,通常在有异常的时候自动被创建并且掷出,可以由new Error(...)建立出来不过很少见。


Contents

物件的内容会储存在物件中某些特定命名的位置上,我们称这些储存在物件中的值为properties

虽然我们说内容是存在於物件之中,但其实这只是一种看起来而已,对JS来说他是以依赖(implementation-dependent)的方式储存并且很有可能不将内容储存在物件容器中只有这些properties的名称储存在容器,而这些properites名称会当作指向储存内容的位置的指针,换句话说储存在物件容器内的properties名称是物件内容的Reference

var myObject = {
	a: 2
};

myObject.a;	// 2
myObject["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"]; // hello
myObject["foobaz"]; // world

Property vs. Method

对於在物件中的function来说,许多开发者将他与property区分开来,我们称它为method,因为以技术上来说function他其实是不属於物件,他在物件中只是以一个Reference的形式储存,所以当物件访问一个function的时候就很像是一个方法(method),虽然这是一个满牵强的理由XD。

Array

阵列也使用[]来访问其中的元素,但是阵列在储存值以及储存位置的结构上更具有组织性,阵列采用数字索引这意味着这个元素被储存的位置,必须是一个非负整数

var myArray = [ "foo", 42, "bar" ];

myArray.length;		// 3
myArray[0];			// "foo"
myArray[2];			// "bar"

在上面有提到其实阵列也是一种物件,所以你也可以对这个阵列增加属性

var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";

myArray.length;	// 3
myArray.baz;	// "baz"

虽然阵列可以达到与物件一样的效果(增加键/值)但是不推荐做这种操作,因为阵列本身有他的用途与使用方法,所以建议用物件来储存键/值而不适用阵列。

还有一个直得注意的地方,虽然我们对myArray添加了属性,但是可以发现myArray.length的长度并没有被改变,但是如果在阵列的属性中添加的值看起来像个数字,则他最终会变成阵列的索引

var myArray = [ "foo", 42, "bar" ];

myArray["3"] = "baz";
myArray.length;	// 4
myArray[3];		// "baz"

除了会更改阵列长度之外,如果添加的数字属性名是已经存在於阵列中的index,则会改变其阵列的内容

var myArray = [ "foo", 42, "bar" ];

myArray["1"] = "baz";
myArray.length;	// 3
myArray[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 }

https://ithelp.ithome.com.tw/upload/images/20201029/201247676vgYevTMVo.png
(图片来源 : [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 }

https://ithelp.ithome.com.tw/upload/images/20201029/20124767YzWOPgJA1n.png
(图片来源 : [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); // false
console.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 中的浅拷贝和深拷贝


<<:  全方位对比:SmartQuery VS FineReport来自报表工程师的经验

>>:  【影片】Windows 搜寻框输入命令快速开启 VSCode (code.cmd.)

Day 02 HTML<表格标签>

表格标签主要用来显示以及展示数据,可用表格标签排版後让数据更容易阅读 1. 表格基础标签简易介绍 (...

Day 23 | 使用ManoMotion制作打地鼠游戏Part1 - 手部侦测及地鼠设定

在上一篇文章介绍了ManoMotion的安装与介绍,今天我们要使用ManoMotion来制作打地鼠游...

3. 用vscode的live server打造方便的开发环境

什麽是live server? 如果有看我之前的文章就会知道它是一个vscode的插件,可以即时预览...

Day 11:将你的 Hexo 部落格部属到 Github Pages

我相今天的篇章是大家期待已久的,因为经过前面十天的努力,今天终於要将我们的部落格公开在世人面前啦!不...

Day8-Go阵列Array

前言 阵列是一种资料结构,里面装载的资料必须是同性质的,不能同时装载着字串又装载着整数,且建立後阵列...