JavaScript中的对象和“类”模式

一、对象

3.1 语法

对象可以通过两种形式定义:声明(文字)形式构造形式
对象的文字语法大概是这样:(使用最多)

var myObj = {
	key: value
	// ...
};

构造形式大概是这样:

var myObj = new Object();
myObj.key = value;

构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字声明中你可以添加多个键 / 值对,但是在构造形式中你必须逐个添加属性。

3.2 类型

对象是 JavaScript 的基础。在 JavaScript 中一共有六种主要类型(术语是“语言类型”):
• string
• number
• boolean
• null
• undefined
• object

注意,简单基本类型(string、boolean、number、null 和 undefined)本身并不是对象
null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行typeof null 时会返回字符串 “object”。实际上,null 本身是基本类型。(原理是这样的,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。)
有一种常见的错误说法是“JavaScript 中万物皆是对象”,这显然是错误的。

JavaScript 中有许多特殊的对象子类型,我们可以称之为复杂基本类型
函数就是对象的一个子类型(从技术角度来说就是“可调用的对象”)。JavaScript 中的函数是“一等公民”,因为它们本质上和普通的对象一样(只是可以调用),所以可以像操作其他对象一样操作函数(比如当作另一个函数的参数)。
数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要稍微复杂一些

内置对象
JavaScript 中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂,我们稍后会详细介绍。
• String
• Number
• Boolean
• Object
• Function
• Array
• Date
• RegExp
• Error
这些内置对象很像其他语言的类型(type)或者类(class)。
在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数由 new 产生的函数调用)来使用,从而可以构造一个对应子类型的新对象。

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
// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]

我们可以认为子类型在内部借用了 Object 中的 toString() 方法

{}+{}   //"[object Object][object Object]"

发生了隐式toString()方法
在这里插入图片描述
必要时语言会自动把字符串字面量转换成一个 String 对象,也就是说你并不需要显式创建一个对象。JavaScript 社区中的大多数人都认为能使用文字形式时就不要使用构造形式

var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"

我们之所以可以在字符串字面量上访问属性和方法,是因为引擎自动把字面量转换成 String 对象,所以可以访问属性和方法

null 和 undefined 没有对应的构造形式,它们只有文字形式。相反,Date 只有构造,没有文字形式。

对于 Object、Array、Function 和 RegExp(正则表达式)来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。在某些情况下,相比用文字形式创建对象,构造形式可以提供一些额外选项。由于这两种形式都可以创建对象,所以我们首选更简单的文字形式建议只在需要那些额外选项时使用构造形式。
Error 对象很少在代码中显式创建,一般是在抛出异常时被自动创建。也可以使用 newError(…) 这种构造形式来创建。

3.3 内容

对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性
需要强调的一点是,当我们说“内容”时,似乎在暗示这些值实际上被存储在对象内部,但是这只是它的表现形式。在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。 存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置

var myObject = {
	a: 2
};
myObject.a; // 2
myObject["a"]; // 2

使用 . 操作符或者 [] 操作符来访问myObject中a位置的值.a 语法通常被称为“属性访问”,[“a”] 语法通常被称为“键访问”。实际上它们访问的是同一个位置,并且会返回相同的值 2,所以这两个术语是可以互换的。我们会使用最常见的术语“属性访问”。

这两种语法的主要区别在于 . 操作符要求属性名满足标识符的命名规范,而 ["…"] 语法可以接受任意 UTF-8/Unicode 字符串作为属性名。举例来说,如果要引用名称为 “Super-Fun!” 的属性,那就必须使用 [“Super-Fun!”] 语法访问,因为 Super-Fun! 并不是一个有效的标识符属性名。

由于 ["…"] 语法使用字符串来访问属性,所以可以在程序中构造这个字符串,比如说:

var myObject = {
	a:2
};
var idx;
if (wantA) {
	idx = "a";
}
// 之后
console.log( myObject[idx] ); // 2

在对象中,属性名永远都是字符串。 如果你使用 string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法

var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
// ==隐式使用toString()方法转换为一个字符串==
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
({}).toString()    //"[object Object]"

在这里插入图片描述

3.3.1 可计算属性名

使用["…"] 这种属性访问语法。如可以使用 myObject[prefix + name]。但是使用文字形式来声明对象时这样做是不行的。
ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名

var prefix = "foo";
var myObject = {
	[prefix + "bar"]:"hello",
	[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
var prefix = "foo";
var myObject = {
	foobar:"hello",
	foobaz: "world"
};
console.log(myObject[prefix+"bar"]); // hello
console.log(myObject[prefix+"baz"]); // world

在这里插入图片描述
可计算属性名最常用的场景可能是 ES6 的符号(Symbol)。它们是一种新的基础数据类型,包含一个不透明且无法预测的值(从技术角度来说就是一个字符串)。一般来说你不会用到符号的实际值(因为理论上来说在不同的 JavaScript 引擎中值是不同的),所以通常你接触到的是符号的名称,比如 Symbol.Something:

var myObject = {
	[Symbol.Something]: "hello world"
}

3.3.2 属性与方法

JavaScript 的语法规范对此也做出了同样的区分。
从技术角度来说,函数永远不会“属于”一个对象,所以把对象内部引用的函数称为“方法”似乎有点不妥。

无论返回值是什么类型,每次访问对象的属性就是属性访问。 如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定 this)。看一个案例加深理解:

function foo() {
	console.log( "foo" );
}
var someFoo = foo; // 对 foo 的变量引用
var myObject = {
	someFoo: foo
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // function foo(){..}

someFoo 和 myObject.someFoo 只是对于【同一个函数】的【不同引用】,并不能说明这个函数是特别的或者“属于”某个对象。如果 foo() 定义时在内部有一个 this 引用,那这两个函数引用的唯一区别就是myObject.someFoo 中的 this 会被隐式绑定到一个对象。无论哪种引用形式都不能称之为“方法”。

最保险的说法可能是,“函数”和“方法”在 JavaScript 中是可以互换的。

ES6 增加了 super 引用,一般来说会被用在 class 中。super的行为似乎更有理由把 super 绑定的函数称为“方法”。但是再说一次,这些只是一些语义(和技术)上的微妙差别,本质是一样的。

即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象——它们只是对于相同函数对象的多个引用。

var myObject = {
	//对象的文字形式中声明函数
	foo: function() {
		console.log( "foo" );
	}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}

3.3.3 数组

数组也支持 [] 访问形式,不过就像我们之前提到过的,数组有一套更加结构化的值存储机制(不过仍然不限制值的类型)。数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是非负整数

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"

可以看到虽然添加了命名属性(无论是通过 . 语法还是 [] 语法),数组的 length 值并未发生变化
完全可以把数组当作一个普通的键 / 值对象来使用,并且不添加任何数值索引,但是这并不是一个好主意。数组和普通的对象都根据其对应的行为和用途进行了优化,所以 最好只用对象来存储键 / 值对,只用数组来存储数值下标 / 值对。

【注意:】 如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性):

var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";  //这里添加的属性“3”,会变成数值下标3

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

3.3.4 复制对象

如何复制一个对象?

function anotherFunction() { /*..*/ }
var anotherObject = {
	c: true
};
var anotherArray = [];
var myObject = {
	a: 2,
	b: anotherObject, // 引用,不是副本!
	c: anotherArray, // 另一个引用!
	d: anotherFunction
};
anotherArray.push( anotherObject, myObject );

如何准确地表示 myObject 的复制呢?

首先,我们应该判断它是浅复制还是深复制。对于 浅拷贝 来说,复制出的新对象中 a 的值会复制旧对象中 a 的值,也就是 2,但是新对象中 b、c、d 三个属性其实只是三个引用,它们和旧对象中 b、c、d 引用的对象是一样的。 对于 深复制 来说,除了复制 myObject 以外还会复制 anotherObject 和 anotherArray。这时问题就来了,anotherArray 引用了 anotherObject 和myObject,所以又需要复制 myObject,这样就会由于循环引用导致死循环
我们是应该检测循环引用并终止循环(不复制深层元素)?还是应当直接报错或者是选择其他方法?
除此之外,我们还不确定“复制”一个函数意味着什么。有些人会通过toString() 来序列化一个函数的源代码(但是结果取决于 JavaScript 的具体实现,而且不同的引擎对于不同类型的函数处理方式并不完全相同)。
那么如何解决这些棘手问题呢?许多 JavaScript 框架都提出了自己的解决办法,但是JavaScript 应当采用哪种方法作为标准呢?在很长一段时间里,这个问题都没有明确的答案。

对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:

var newObj = JSON.parse( JSON.stringify( someObj ) );

相比深复制,浅复制非常易懂并且问题要少得多,所以 ES6 定义了Object.assign(…) 方法来实现浅复制Object.assign(…) 方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。 它会遍历一个或多个源对象的所有可枚举(enumerable,参见下面的代码)的自有键(owned key,很快会介绍)并把它们复制(使用 = 操作符赋值)到目标对象,最后返回目标对象,就像这样:

var newObj = Object.assign( {}, myObject );

newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true

需要注意的一点是,由于 Object.assign(…) 就是使用 = 操作符来赋值,所以源对象属性的一些特性(比如 writable)不会被复制到目标对象

  • 最后关于深浅复制的具体实现请看🔍深浅拷贝

3.3.5 属性描述符

var myObject = {
	a:2
};

Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// 	value: 2,
// 	writable: true,
// 	enumerable: true,
// 	configurable: true
// }

它还包含另外三个特性:writable(可写)、enumerable(可枚举)和 configurable(可配置)

在创建普通属性时属性描述符会使用默认值,我们也可以使用Object.defineProperty(…)添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。

var myObject = {};
Object.defineProperty( myObject, "a", {
	value: 2,
	writable: true,
	configurable: true,   ///
	enumerable: true
} );
myObject.a; // 2
  1. Writable
    writable 决定是否可以修改属性的值
var myObject = {};
Object.defineProperty( myObject, "a", {
	value: 2,
	writable: false, // 不可写!
	configurable: true,
	enumerable: true
} );
myObject.a = 3;
myObject.a; // 2

如果在严格模式下,这种方法会出错。

myObject.a = 3; // TypeError  TypeError 错误表示我们无法修改一个不可写的属性。
  1. Configurable
    只要属性是可配置的,就可以使用 defineProperty(…) 方法来修改属性描述符
var myObject = {
	a:2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty( myObject, "a", {
	value: 4,
	writable: true,  //可以修改
	configurable: false,   // 不可配置!
	enumerable: true
} );

myObject.a; // 4
myObject.a = 5;   //可修改
myObject.a; // 5

Object.defineProperty( myObject, "a", {
	value: 6,
	writable: true,
	configurable: true,
	enumerable: true
} ); // TypeError

不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。注意:如你所见,把 configurable 修改成false 是单向操作,无法撤销!

要注意有一个小小的例外:即便属性是 configurable:false,我们还是可以把 writable 的状态由 true 改为 false,但是无法由 false 改为 true

除了无法修改,configurable:false 还会禁止删除这个属性

var myObject = {
	a:2
};
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty( myObject, "a", {
	value: 2,
	writable: true,
	configurable: false,
	enumerable: true
} );
myObject.a; // 2
delete myObject.a;//静默失败
myObject.a; // 2

delete 只用来直接删除对象的(可删除)属性。如果对象的某个属性是某个对象 / 函数的最后一个引用者,对这个属性执行 delete 操作之后,这个未引用的对象 / 函数就可以被垃圾回收。但是,不要把 delete 看作一个释放内存的工具(就像 C/C++ 中那样),它就是一个删除对象属性的操作,仅此而已。

  1. Enumerable
    这里我们要介绍的最后一个属性描述符(还有两个,我们会在介绍 getter 和 setter 时提到)是 enumerable。
    从名字就可以看出,这个描述符控制的是属性是否会出现在对象的属性枚举中比如说for…in 循环。如果把 enumerable 设置成 false,这个属性就不会出现在枚举中,虽然 仍然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。

3.3.6 不变性

很重要的一点是,所有的方法创建的都是浅不变性,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的:

myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]

假设代码中的 myImmutableObject 已经被创建而且是不可变的,但是 为了保护它的内容myImmutableObject.foo,你还需要使用下面的方法让 foo 也不可变。 在 JavaScript 程序中很少需要深不可变性。

  1. 对象常量
    结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除):
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
	value: 42,
	writable: false,
	configurable: false
} );
  1. 禁止扩展
    如 果 你 想 禁 止 一 个 对 象 添 加 新 属 性 并 且 保 留 已 有 属 性, 可 以 使 用 Object.preventExtensions(…)
var myObject = {
	a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;  
myObject.b; // undefined  禁止添加,并保留已有属性
  1. 密封(Object.preventExtensions(…) + configurable:false)
    Object.seal(…) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(…) 并把所有现有属性标记为configurable:false

所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

  1. 冻结(Object.seal(…) + writable:false)
    Object.freeze(…) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(…) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值
    这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。
    你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用Object.freeze(…),然后遍历它引用的所有对象并在这些对象上调用Object.freeze(…)。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。

3.3.7 [[Get]]

var myObject = {
	a: 2
};
myObject.a; // 2

myObject.a 是一次属性访问,但是 这条语句并不仅仅是在 myObjet 中查找名字为 a 的属性
在语言规范中,myObject.a 在 myObject 上实际上是实现了 [[Get]] 操作(有点像函数调用:[ [ Get ] ] ())对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。

然而,如果没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另外一种非常重要的行为。我们会在第 五章中介绍这个行为(其实就是遍历可能存在的 [[Prototype]] 链,也就是原型链)。
如果无论如何都没有找到名称相同的属性,那 [[Get]] 操作会返回值undefined:

var myObject = {
	a:2
};
myObject.b; // undefined

注意,这种方法和访问变量时是不一样的。如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回 undefined(在对象属性中,这个不存在的变量会返回undefined),而是会抛出一个 ReferenceError 异常(不用对象访问就会返回refererenceError):

var myObject = {
	a: undefined
};
myObject.a; // undefined
//==注意下面两者区别==
myObject.b; // undefined
c;          //ReferenceError: c is not defined

在这里插入图片描述
从返回值的角度来说,这两个引用没有区别——它们都返回了 undefined。然而,尽管乍看之下没什么区别,实际上底层的 [[Get]] 操作对 myObject.b 进行了更复杂的处理
由于仅根据返回值无法判断出到底变量的值为 undefined 还是变量不存在,所以 [[Get]]操作无法返回某个特定值而返回默认的 undefined

稍后3.3.10节我们会介绍如何区分这两种情况

3.3.8 [[Put]]

[[Put]] 被触发时,实际的行为取决于许多因素,包括 对象中是否已经存在这个属性这是最重要的因素)。
如果已经存在这个属性,[[Put]] 算法大致会检查下面这些内容。

  1. 属性是否是访问描述符(参见 3.3.9 下一节)?如果是并且存在 setter 就调用 setter。
  2. 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
  3. 如果都不是,将该值设置为属性的值。

如果对象中不存在这个属性,[[Put]] 操作会更加复杂。我们会在第 五 章讨论 [[Prototype]] 时详细进行介绍。

3.3.9 Getter和Setter

对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。

可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏函数,会在设置属性值时调用。
当你给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为**“访问描述符”**(和“数据描符”相对)。【对于访问描述符来说,JavaScript 会忽略它们的 value 和writable 特性取而代之的是关心 set 和 get(还有configurable 和 enumerable)特性。

var myObject = {
	// 给 a 定义一个 getter
	get a() {
		return 2;
	}
};
Object.defineProperty(
	myObject, // 目标对象
	"b", // 属性名
	{ // 描述符
		// 给 b 设置一个 getter
		get: function(){ return this.a * 2 },
		// 确保 b 会出现在对象的属性列表中
		enumerable: true
	}
);
myObject.a; // 2
myObject.b; // 4

不管是对象文字语法中的 get a() { … },还是 defineProperty(…) 中的显式定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值:

var myObject = {
	// 给 a 定义一个 getter
	get a() {
		return 2;
	}
};
myObject.a = 3; //只有get,忽略赋值操作
myObject.a; // 2

由于我们只定义了 a 的 getter,所以对 a 的值进行设置时 set 操作会忽略赋值操作,不会抛出错误。而且即便有合法的 setter,由于我们自定义的 getter 只会返回 2,所以 set 操作是没有意义的。

为了让属性更合理,还应当定义 setter,和你期望的一样,setter 会覆盖单个属性默认的[[Put]](也被称为赋值)操作。通常来说 getter 和 setter 是成对出现的(只定义一个的话通常会产生意料之外的行为):

var myObject = {
	// 给 a 定义一个 getter
	get a() {
		return this._a_;
	},
	// 给 a 定义一个 setter
	set a(val) {
		this._a_ = val * 2;
	}
};
myObject.a = 2;
myObject.a; // 4

实际上我们把赋值([[Put]])操作中的值 2 存储到了另一个变量_a_ 中。名称_a_ 只是一种惯例,没有任何特殊的行为——和其他普通属性一样。

3.3.10 存在性

在3.3.7节末尾遗留了一个问题,即 myObject.a 的属性访问返回值可能是undefined,但是这个值有可能是属性中存储的 undefined,也可能是因为属性不存在所以返回 undefined。那么如何区分这两种情况呢?
我们可以在不访问属性值的情况下判断对象中是否存在这个属性:

var myObject = {
	a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false

in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中(参见第 5 章)。相比之下,hasOwnProperty(…) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。

所 有 的 普 通 对 象 都 可 以 通 过 对 于 Object.prototype 的 委 托( 参 见 第 5 章 ) 来 访 问hasOwnProperty(…),但是有的对象可能没有连接到 Object.prototype(通过 Object.create(null) 来创建——参见第 5 章)。在这种情况下,形如 myObejct.hasOwnProperty(…)就会失败。
这 时 可 以 使 用 一 种 更 加 强 硬 的 方 法 来 进 行 判 断:Object.prototype.hasOwnProperty.call(myObject,“a”),它借用基础的hasOwnProperty(…) 方法并把它显式绑定(参见第 2章)到 myObject 上。

看起来 in 操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。对于数组来说这个区别非常重要,4 in [2, 4, 6] 的结果并不是你期待的 True,因为 [2, 4, 6] 这个数组中包含的属性名是 0、1、2,没有 4。

  1. 枚举
var myObject = { };
Object.defineProperty(
	myObject,
	"a",
	// 让 a 像普通属性一样可以枚举
	{ enumerable: true, value: 2 }
);
Object.defineProperty(
	myObject,
	"b",
	// 让 b 不可枚举
	{ enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true
// .......
for (var k in myObject) {
	console.log( k, myObject[k] );
}
// "a" 2

可以看到,myObject.b 确实存在并且有访问值,但是却不会出现在 for…in 循环中(尽管可以通过 in 操作符来判断是否存在)。原因是 “可枚举”就相当于“可以出现在对象属性的【遍历】中”。

数组上应用 for…in 循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性最好只在对象上应用for…in 循环,如果要遍历数组就使用传统的 for 循环来遍历数值索引。

另一种方式来区分属性是否可枚举:

var myObject = { };
Object.defineProperty(
	myObject,
	"a",
	// 让 a 像普通属性一样可以枚举
	{ enumerable: true, value: 2 }
);
Object.defineProperty(
	myObject,
	"b",
	// 让 b 不可枚举
	{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

propertyIsEnumerable(…) 会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足 enumerable:true。
Object.keys(…) 会返回一个数组,包含所有【可枚举属性】Object.getOwnPropertyNames(…)会返回一个数组,包含【所有属性】,无论它们是否可枚举。

in 和 hasOwnProperty(…) 的【区别在于】 是否查找 [[Prototype]] 链,然而,Object.keys(…)和 Object.getOwnPropertyNames(…) 都只会查找对象直接包含的属性

3.4 遍历

for…in 循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。但是如何遍历属性的值呢?

对于数值索引的数组来说,可以使用标准的 for 循环来遍历值:

var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
	console.log( myArray[i] );
}
// 1 2 3

这实际上并不是在遍历值,而是遍历下标来指向值,如 myArray[i]。

ES5 中增加了一些数组的辅助迭代器,包括 forEach(…)、every(…) 和 some(…)。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同
forEach(…) 会遍历数组中的所有值并忽略回调函数的返回值。every(…) 会一直运行直到回调函数返回 false(或者“假”值),some(…) 会一直运行直到回调函数返回 true(或者“真”值)。

every(…) 和 some(…) 中特殊的返回值和普通 for 循环中的 break 语句类似,它们会提前终止遍历

使用 for…in 遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。

遍历数组下标时采用的是数字顺序(for 循环或者其他迭代器),但是遍历对象属性时的顺序是不确定的

ES6 增加了一种用来遍历数组的 for…of 循环语法(如果对象本身定义了迭代器的话也可以遍历对象),它可以直接遍历值:
for…of 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next() 方法来遍历所有返回值。
数组有内置的 @@iterator,因此 for…of 可以直接应用在数组上。我们使用内置的 @@iterator 来手动遍历数组,看看它是怎么工作的:

var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }

我们使用 ES6 中的符号 Symbol.iterator 来获取对象的 @@iterator 内部属性。引用类似 iterator 的特殊属性时要使用符号名,而不是符号包含的值。此外,虽然看起来很像一个对象,但是 @@iterator 本身并不是一个迭代器对象,而是一个返回迭代器对象的【函数】——这点非常精妙并且非常重要。

调用迭代器的 next() 方法会返回形式为 { value: … , done: … } 的值,value 是当前的遍历值,done 是一个布尔值,表示是否还有可以遍历的值。

注意,和值“3”一起返回的是 done:false,乍一看好像很奇怪,你必须再调用一次next() 才能得到 done:true从而确定完成遍历。这个机制和 ES6 中发生器函数的语义相关,不过已经超出了我们的讨论范围。

和数组不同,普通的对象没有内置的 @@iterator,所以无法自动完成 for…of 遍历。之所以要这样做,有许多非常复杂的原因,不过简单来说,这样做是为了避免影响未来的对象类型。

当然,你可以给任何想遍历的对象定义 @@iterator

var myObject = {
	a: 2,
	b: 3
};
//给对象定义@@iterator
Object.defineProperty( myObject, Symbol.iterator, {
	enumerable: false,
	writable: false,
	configurable: true,
	value: function() {
		var o = this;
		var idx = 0;
		var ks = Object.keys( o );
		return {
			next: function() {
				return {
					value: o[ks[idx++]],
					done: (idx > ks.length)
				};
			}
		};
	}
} );

// 手动遍历 myObject
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }

// 用 for..of 遍历 myObject
for (var v of myObject) {
	console.log( v );
}
// 2
// 3

我们使用 Object.defineProperty(…) 定义了我们自己的 @@iterator主要是为了让它不可枚举),不过注意,我们把符号当作可计算属性名(本章之前有介绍)。此外,也可以直接在定义对象时进行声明,比如 var myObject = { a:2, b:3, [Symbol.iterator]: function() { /* … */ } }。

for…of 循环每次调用 myObject 迭代器对象的 next() 方法时,内部的指针都会向前移动并返回对象属性列表的下一个值(再次提醒需要注意遍历对象属性 / 值时的顺序)。

实际上,你甚至可以定义一个“无限”迭代器,它永远不会“结束”并且总会返回一个新值(比如随机数、递增值、唯一标识符,等等)。你可能永远不会在 for…of 循环中使用这样的迭代器,因为它永远不会结束,你的程序会被挂起:

var randoms = {
	[Symbol.iterator]: function() {
		return {
			next: function() {
				return { value: Math.random() };
			}
		};
	}
};
var randoms_pool = [];
for (var n of randoms) {
	randoms_pool.push( n );
	
	// 防止无限运行!防止程序被挂起。
	if (randoms_pool.length === 100) break;
}

3.5 小结

  • JavaScript 中的对象有字面形式(比如 var a = { … })和构造形式(比如 var a = new Array(…))。字面形式更常用,不过有时候构造形式可以提供更多选项。
  • 许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者是 7 个,取决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。
  • 对象就是键 / 值对的集合。可以通过 .propName 或者 [“propName”] 语法来获取属性值。访问属性时,引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]]),[[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]]链(参见第 5 章)。
  • 属性的特性可以通过属性描述符来控制,比如 writable 和 configurable。此外,可以使用Object.preventExtensions(…)、Object.seal(…) 和Object.freeze(…) 来设置对象(及其属性)的不可变性级别。
  • 属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是可枚举或者不可枚举的,这决定了它们是否会出现在 for…in 循环中。
  • 你可以使用 ES6 的 for…of 语法来遍历数据结构(数组、对象,等等)中的值,for…of会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。

四、混合对象“类”

本章节介绍和类相关的面向对象编程。(面向对象编程理论+介绍混入时会把这些概念落实到 JavaScript 代码上)
面向类的设计模式:实例化(instantiation)、继承(inheritance)和
(相对)多态(polymorphism)。
这些概念实际上无法直接对应到 JavaScript 的对象机制,对此介绍许多 JavaScript 开发者所使用的解决方法(比如混入,mixin)

4.1 类理论

类 / 继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法
面向对象编程强调的是数据和操作数据的行为本质上是互相关联的(当然,不同的数据有不同的行为),因此好的设计就是把数据以及和它相关的行为打包(或者说封装)起来。这在正式的计算机科学中有时被称为数据结构
举例:所有字符串都是 String 类的一个实例,也就是说它是一个包裹,包含字符数据和我们可以应用在数据上的函数。
我们来看一个常见的例子,“汽车”可以被看作“交通工具”的一种特例,后者是更广泛的类。
我们可以在软件中定义一个 Vehicle 类和一个 Car 类来对这种关系进行建模。
Car 的定义就是对通用 Vehicle 定义的特殊化
虽然 Vehicle 和 Car 会定义相同的方法,但是实例中的数据可能是不同的,比如每辆车独一无二的 VIN(Vehicle Identification Number,车辆识别号码),等等。
这就是类、继承和实例化

类的另一个核心概念是 多态,这个概念是说父类的通用行为可以被子类用更特殊的行为重写。实际上,相对多态性允许我们从重写行为中引用基础行为。

类理论强烈建议父类和子类使用相同的方法名来表示特定的行为,从而让子类重写父类。我们之后会看到,在 JavaScript 代码中这样做会降低代码的可读性和健壮性。

4.1.1 “类”设计模式

过程化编程,这种代码只包含过程(函数)调用,没有高层的抽象。

如果你有函数式编程(比如 Monad)的经验就会知道类也是非常常用的一种设计模式。但是对于其他人来说,这可能是第一次知道类并不是必须的编程基础,而是一种可选的代码抽象

4.1.2 JavaScript中的“类”

在相当长的一段时间里,JavaScript 只有一些近似类的语法元素(比如 new 和 instanceof),不过在后来的 ES6 中新增了一些元素,比如 class 关键字。但是这并不是意味着 JavaScript 中实际上有类。

由于类是一种设计模式,所以你可以用一些方法(本章之后会介绍)近似实现类的功能。为了满足对于类设计模式的最普遍需求,JavaScript 提供了一些近似类的语法
虽然有近似类的语法,但是 JavaScript 的机制似乎一直在阻止你使用类设计模式。在近似类的表象之下,JavaScript 的机制其实和类完全不同。语法糖和(广泛使用的)JavaScript“类”库试图掩盖这个现实,但是你迟早会面对它:其他语言中的类和 JavaScript中的“类”并不一样

【总结一下】,在软件设计中类是一种可选的模式,你需要自己决定是否在 JavaScript 中使用它。由于许多开发者都非常喜欢面向类的软件设计,我们会在本章的剩余部分中介绍如何在 JavaScript 中实现类以及存在的一些问题。

4.2 类的机制

在许多面向类的语言中,“标准库”会提供 Stack 类,它是一种“栈”数据结构(支持压入、弹出,等等)。

但是在这些语言中,你实际上并不是直接操作 Stack(除非创建一个静态类成员引用,这超出了我们的讨论范围)。Stack 类仅仅是一个抽象的表示,它描述了所有“栈”需要做的事,但是它本身并不是一个“栈”。你必须先实例化 Stack 类然后才能对它进行操作。

4.2.1 建造

“类”和“实例”的概念来源于房屋建造。
一个类就是一张蓝图。
把类和实例对象之间的关系看作是直接关系而不是间接关系通常更有助于理解。类通过复制操作被实例化为对象形式(复制操作,如图):
在这里插入图片描述

4.2.2 构造函数

类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数。这个方法的任务就是初始化实例需要的所有信息(状态)。

//伪代码
class CoolGuy {
	specialTrick = nothing
	CoolGuy( trick ) {
		specialTrick = trick
	}
	showOff() {
		output( "Here's my trick: ", specialTrick )
	}
}

//调用类构造函数生成一个实例
Joe = new CoolGuy( "jumping rope" )
Joe.showOff() // 这是我的绝技:跳绳

注意,CoolGuy 类有一个 CoolGuy() 构造函数,执行 new CoolGuy() 时实际上调用的就是它。构造函数会返回一个对象(也就是类的一个实例),之后我们可以在这个对象上调用showOff() 方法,来输出指定 CoolGuy 的特长。

类构造函数属于类,而且通常和类同名。此外,构造函数大多需要用 new 来调,这样语言引擎才知道你想要构造一个新的类实例。

4.3 类的继承

定义好一个子类之后,相对于父类来说它就是一个独立并且完全不同的类。 子类会包含父类行为的原始副本,但是也可以重写所有继承的行为甚至定义新行为
非常重要的一点是,我们讨论的父类和子类并不是实例。父类和子类的比喻容易造成一些误解,实际上我们应当把父类和子类称为父类 DNA 和子类 DNA我们需要根据这些DNA 来创建(或者说实例化)一个人,然后才能和他进行沟通。

下面来看一个非常典型的讲解继承的例子。

//伪代码
class Vehicle {
	engines = 1
	ignition() {
		output( "Turning on my engine." );
	}
	drive() {
		ignition();
		output( "Steering and moving forward!" )
	}
}
//定义了两类具体的交通工具:Car 和 SpeedBoat。
//它们都从 Vehicle 继承了通用的特性并根据自身类别修改了某些特性。
class Car inherits Vehicle {
	wheels = 4
	drive() {
		inherited:drive()
		output( "Rolling on all ", wheels, " wheels!" )
	}
}
class SpeedBoat inherits Vehicle {
	engines = 2
	ignition() {
		output( "Turning on my ", engines, " engines." )
	}
	pilot() {
		inherited:drive()
		output( "Speeding through the water with ease!" )
	}
}

4.3.1 多态

Car 重写了继承自父类的 drive() 方法,但是之后 Car 调用了inherited:drive() 方法,这表明 Car 可以引用继承来的原始drive() 方法。 快艇的 pilot() 方法同样引用了原始drive() 方法。这个技术被称为多态或者虚拟多态。在本例中,更恰当的说法是相对多态

多态的另一个方面是,在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义。

在传统的面向类的语言中 super 还有一个功能,就是从子类的构造函数中通过super 可以直接调用父类的构造函数。通常来说这没什么问题,因为对于真正的类来说,构造函数是属于类的。然而,在 JavaScript 中恰好相反——实际上“类”是属于构造函数的(类似 Foo.prototype… 这样的类型引用)。由于JavaScript 中父类和子类的关系只存在于两者构造函数对应的 .prototype 对象中,因此它们的构造函数之间并不存在直接联系,从而无法简单地实现两者的相对引用(在 ES6 的类中可以通过 super 来“解决”这个问题)。

我们可以在 ignition() 中看到多态非常有趣的一点。在 pilot() 中通过相对多态引用了(继承来的)Vehicle 中的 drive()。但是那个 drive() 方法直接通过名字(而不是相对引用)引用了 ignotion() 方法。

那么语言引擎会使用哪个 ignition() 呢,Vehicle 的还是 SpeedBoat 的?实际上它会使用SpeedBoat 的 ignition()。如果你直接实例化了 Vehicle 类然后调用它的 drive(),那语言引擎就会使用 Vehicle 中的 ignition() 方法。

换言之,ignition() 方法定义的多态性取决于【你是在哪个类的实例中引用它】。

这似乎是一个过于深入的学术细节,但是只有理解了这个细节才能理解 JavaScript 中类似(但是并不相同)的 [[Prototype]] 机制
在子类(而不是它们创建的实例对象!)中也可以相对引用它继承的父类,这种相对引用通常被称为 super
在这里插入图片描述
子类 Bar 应当可以通过相对多态引用(或者说 super)来访问父类 Foo 中
的行为。需要注意,子类得到的仅仅是继承自父类行为的一份副本。子类对继承到的一个方法进行“重写”,不会影响父类中的方法,这两个方法互不影响,因此才能使用相对多态引用访问父类中的方法(如果重写会影响父类的方法,那重写之后父类中的原始方法就不存在了,自然也无法引用)。

多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是 复制

4.3.2 多重继承

有些面向类的语言允许你继承多个“父类”。多重继承意味着所有父类的定义都会被复制到子类中。

存在的问题:

  • 如果两个父类中都定义了 drive() 方法的话,子类引用的是哪个呢?
  • 钻石问题的变种。如图,如果 A 中有 drive() 方法并且 B 和 C 都重写了这个方法(多态),那当 D 引用 drive() 时应当选择哪个版本呢(B:drive() 还是 C:drive())?
    在这里插入图片描述
    JavaScript 要简单得多:它本身并不提供“多重继承”功能。

4.4 混入

在继承或者实例化时,JavaScript 的对象机制并不会自动执行复制行为。简单来说,JavaScript 中只有对象,并不存在可以被实例化的“类”。一个对象并【不会被复制】到其他对象,它们会【被关联】起来

混入就是用来模拟类的复制行为。

4.4.1 显式混入

// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj, targetObj ) {
	for (var key in sourceObj) {
		// 只会在不存在的情况下复制
		if (!(key in targetObj)) {
			targetObj[key] = sourceObj[key];
		}
	}
	return targetObj;
}
var Vehicle = {
	engines: 1,
	ignition: function() {
		console.log( "Turning on my engine." );
	},
	drive: function() {
		this.ignition();
		console.log( "Steering and moving forward!" );
	}
};
var Car = mixin( Vehicle, {
	wheels: 4,
	drive: function() {
		Vehicle.drive.call( this );
		console.log(
			"Rolling on all " + this.wheels + " wheels!"
		);
	}
} );

有一点需要注意,我们处理的已经不再是类了,因为 在 JavaScript 中不存在类,Vehicle 和 Car 都是对象。

现在 Car 中就有了一份 Vehicle 属性和函数的副本了。从技术角度来说,函数实际上没有被复制,复制的是函数引用。 所以,Car 中的属性 ignition 只是从 Vehicle 中复制过来的对于 ignition() 函数的引用。相反,属性 engines 就是直接从 Vehicle 中复制了值 1。

Car 已经有了 drive 属性(函数),所以这个属性引用并没有被 mixin 重写,从而保留了Car 中定义的同名属性,实现了“子类”对“父类”属性的重写(参见 mixin(…) 例子中的 if 语句)。

  1. 再说多态
    我们来分析一下这条语句:Vehicle.drive.call( this )。这就是我所说的显式多态。还记得吗,在之前的伪代码中对应的语句是 inherited:drive(),我们称之为相对多态
    JavaScript(在 ES6 之前)并没有相对多态的机制。所以,由于 Car 和Vehicle 中都有 drive() 函数,为了指明调用对象,我们必须使用绝对(而不是相对)引用。我们通过名称显式指定 Vehicle 对象并调用它的 drive() 函数。

但是如果直接执行 Vehicle.drive(),函数调用中的 this会被绑定到 Vehicle 对象而不是Car 对象(参见第 2 章),这并不是我们想要的。因此,我们会使用 .call(this)(参见第 2章)来确保 drive() 在 Car 对象的上下文中执行。

如果函数 Car.drive() 的名称标识符并没有和 Vehicle.drive() 重叠(或者说“屏蔽”;参见第 5 章)的话,我们就不需要实现方法多态,因为调用mixin(…) 时会把函数 Vehicle.drive() 的引用复制到 Car 中,因此我们可以直接访问 this.drive()。正是由于存在标识符重叠,所以必须使用更加复杂的显式伪多态方法。

JavaScript 中(由于屏蔽)使用显式伪多态会在所有需要使用(伪)多态引用的地方创建一个函数关联,这会极大地增加维护成本。此外,由于显式伪多态可以模拟多重继承,所以它会进一步增加代码的复杂度和维护难度。

使用伪多态通常会导致代码变得更加复杂、难以阅读并且难以维护,因此应当尽量避免使用显式伪多态,因为这样做往往得不偿失。

  1. 混合复制
    回顾一下之前提到的 mixin(…) 函数:
// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj, targetObj ) {
	for (var key in sourceObj) {
		// 只会在不存在的情况下复制
		if (!(key in targetObj)) {
			targetObj[key] = sourceObj[key];
		}
	}
	return targetObj;
}

我们来分析一下 mixin(…) 的工作原理。它会遍历 sourceObj(本例中是 Vehicle)的属性,如果在 targetObj(本例中是 Car)没有这个属性就会进行复制。由于我们是在目标对象初始化之后才进行复制,因此一定要小心不要覆盖目标对象的原有属性。

如果我们是先进行复制然后对 Car 进行特殊化的话,就可以跳过存在性检查。不过这种方法并不好用并且效率更低,所以不如第一种方法常用:

// 另一种混入函数,可能有重写风险
function mixin( sourceObj, targetObj ) {
	for (var key in sourceObj) {
		targetObj[key] = sourceObj[key];
	}
	return targetObj;
}
var Vehicle = {
	// ...
};
// 首先创建一个空对象并把 Vehicle 的内容复制进去
var Car = mixin( Vehicle, { } );
// 然后把新内容复制到 Car 中
mixin( {
	wheels: 4,
	drive: function() {
		// ...
	}
}, Car );

这两种方法都可以把不重叠的内容从 Vehicle 中显性复制到 Car 中。“混入”这个名字来源于这个过程的另一种解释:Car 中混合了 Vehicle 的内容,就像你把巧克力片混合到你最喜欢的饼干面团中一样。

复制操作完成后,Car 就和 Vehicle 分离了,向 Car 中添加属性不会影响 Vehicle,反之亦然。

实际上,在复制完成之后两者之间仍然有一些巧妙的方法可以“影响”到对方,例如引用同一个对象(比如一个数组)。

由于两个对象引用的是同一个函数,因此这种复制(或者说混入)实际上并不能完全模拟面向类的语言中的复制。

JavaScript 中的函数无法(用标准、可靠的方法)真正地复制,所以你只能复制对共享函数对象的引用(函数就是对象;参见第 3 章)。如果你修改了共享的函数对象(比如ignition()),比如添加了一个属性,那 Vehicle 和 Car 都会受到影响。

显式混入是 JavaScript 中一个很棒的机制,不过它的功能也没有看起来那么强大。虽然它可以把一个对象的属性复制到另一个对象中,但是这其实并不能带来太多的好处,无非就是少几条定义语句,而且还会带来我们刚才提到的函数对象引用问题

如果你向目标对象中显式混入超过一个对象,就可以部分模仿多重继承行为,但是仍没有直接的方式来处理函数和属性的同名问题。有些开发者 / 库提出了“晚绑定”技术和其他的一些解决方法,但是从根本上来说,使用这些“诡计”通常会(降低性能并且)得不偿失。

一定要注意,只在能够提高代码可读性的前提下使用显式混入,避免使用增加代码理解难度或者让对象关系更加复杂的模式。

  1. 寄生继承
    显式混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的。
    工作原理如下:
//“传统的 JavaScript 类”Vehicle
function Vehicle() {
	this.engines = 1;
}
Vehicle.prototype.ignition = function() {
	console.log( "Turning on my engine." );
};
Vehicle.prototype.drive = function() {
	this.ignition();
	console.log( "Steering and moving forward!" );
};

//“寄生类”Car
function Car() {
	// 首先,car 是一个 Vehicle
	var car = new Vehicle();
	// 接着我们对 car 进行定制
	car.wheels = 4;
	// 保存到 Vehicle::drive() 的特殊引用
	var vehDrive = car.drive;
	// 重写 Vehicle::drive()
	car.drive = function() {
		vehDrive.call( this );
		console.log(
			"Rolling on all " + this.wheels + " wheels!"
		);
	return car;
}
var myCar = new Car();
myCar.drive();
// 发动引擎。
// 手握方向盘!
// 全速前进!

调用 new Car() 时会创建一个新对象并绑定到 Car 的 this 上(参见第 2章)。但是因为我们没有使用这个对象而是返回了我们自己的 car 对象,所以最初被创建的这个对象会被丢弃,因此可以不使用 new 关键字调用Car()。这样做得到的结果是一样的,但是可以避免创建并丢弃多余的对象。

4.4.2 隐式混入

隐式混入和之前提到的显式伪多态很像,因此也具备同样的问题。

var Something = {
	cool: function() {
		this.greeting = "Hello World";
		this.count = this.count ? this.count + 1 : 1;
	}
};

Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
	cool: function() {
		// 隐式把 Something 混入 Another
		Something.cool.call( this );
	}
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1(count 不是共享状态)

通过在构造函数调用或者方法调用中使用 Something.cool.call( this ),我们实际上“借用”了函数 Something.cool() 并在 Another 的上下文中调用了它(通过 this 绑定;第 2 章)。最终的结果是 Something.cool() 中的赋值操作都会应用在 Another 对象上而不是Something 对象上。

因此,我们把 Something 的行为“混入”到了 Another 中。
虽然这类技术利用了 this 的重新绑定功能,但是 Something.cool.call( this ) 仍然无法变成相对(而且更灵活的)引用,所以使用时千万要小心。通常来说,尽量避免使用这样的结构,以保证代码的整洁和可维护性。

4.5 小结

  • 类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有类似的语法,但是和其他语言中的类完全不同。
  • 类意味着复制。
  • 传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。
  • 多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。
  • JavaScript 并不会(像类那样)自动创建对象的副本。
  • 混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,比如显式伪多态(OtherObj.methodName.call(this, …)),这会让代码更加难懂并且难以维护。
  • 此外,显式混入实际上无法完全模拟类的复制行为,因为对象(和函数!别忘了函数也是对象)只能复制引用,无法复制被引用的对象或者函数本身。忽视这一点会导致许多问题。

总地来说,在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋下更多的隐患。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值