[JS] You Don't Know JavaScript [this & Object Proto

前言

我们在前面几章中介绍了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种主要类型之一。

stringnumberbooleannullundefinedobject

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

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

Built-in Objects

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

StringNumberBooleanObjectFunctionArrayDateRegExpError
但是在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建立ObjectsArrayFunctionRegExps他们都是物件。

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 }

http://img2.58codes.com/2024/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 }

http://img2.58codes.com/2024/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); // 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 中的浅拷贝和深拷贝


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章