这标题咋一股黄油味儿

ノ○と皇○と野良○ハート

Javascript中有很多东西都是对象 (object) ,比如字符串数字数组等。万物基于object。

既然如此重要,那就先从它开始吧。

对象的初始化

对象可以通过对象字面量表示法 (object literal notation) 进行初始化,它由逗号分隔的若干组键值对组成,以大括号闭合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var o = {
a: 12,
b: 'Misaka',
c: {},
b: 'Mikoto' // 可以在一个文本符号内重复相同名字属性,之后的会覆盖之前的同名属性,o.b值为'Mikoto'
};

var a = 12, b = 'Misaka', c = {};
var o = {
a: a,
b: b,
c: c
};

var o = {
// 可以将属性指向函数,对象内的函数称为它的方法 (method)
f: function (p) {
return p;
},
get m() {}, // o.m的getter
set m() {} // o.m的setter
};

ES5的严格模式下,不允许在一个文本符号内重复相同名字属性。ES6放开了这一限制:

1
2
3
4
5
6
// 在ES5的严格模式中会抛出错误,在ES6中则不会
'use strict'
var o = {
a: 1,
a: 2
}

ES6中,有一些不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var a = 12, b = 'Misaka', c = {};
var o = { a, b, c }; // 省略写法,相当于 var o = { a: a, b: b, c: c };

// 支持spread操作符,将可枚举属性 (enumerable) 复制到新的对象中
// 但要注意,spread操作符和Object.assign()效果相似,只会进行浅复制
// 不过Object.assign()会触发setter,而spread操作符不会
var o2 = {
...o,
d: '=A='
};
// o2值为{ a: 12, b: 'Misaka', c: {}, d: '=A=' }

var o = {
function f(p) { // 省略写法,相当于f: function (p) { return p; }
return p;
}
};

var prefix = 'Misaka';
var o = {
[prefix + 30]: 'gugugu' // 可计算的属性名 (computed property name) ,o.Misaka30值为'gugugu'
};

JSON与对象字面量表示法虽然看起来相似,但也有不同之处:

  • JSON的格式要求与对象字面量表示法不同,例如要求属性名必须使用双引号,并且不能使用省略写法
  • JSON中定义的值只能是string、number、array、bool、null以及JSON对象,其它的比如Date经过JSON.stringify()时会被解析为字符串
  • JSON中对象不能定义方法
  • JSON.parse()不支持可计算的属性名

除了使用对象字面量表示法,还可以通过new Object()Object.create()方法创建新对象。

对象与原型

Javascript是基于原型的语言,虽然也有继承,不过和基于类的继承不太一样。就算ES6引入了class句法,也不过只是个语法糖,当然只谈体验的话确实比之前拿function捏oop好写好看了一些。MDN上也表示这让习惯了基于类的开发者感到一脸懵比。

JavaScript is a bit confusing for developers experienced in class-based languages (like Java or C++), as it is dynamic and does not provide a class implementation per se (the class keyword is introduced in ES2015, but is syntactical sugar, JavaScript remains prototype-based).

Source: Inheritance and the prototype chain, MDN web docs

既然万物基于object, 使用原型模式和对象就能实现继承。

每个对象都有私有属性与另一个被称为原型的对象相链接。比如声明一个字符串变量,字面量表示法会自动生成字符串对象,这个字符串的原型就是String,这样一来,即使开发者没给字符串变量定义返回字符串长度的属性,也可以使用继承自原型的String.prototype.length

1
2
var name = 'Misaka030';
console.log(name.length); // 输出9

当然,这个属性只是继承自字符串对象,声明的字符串变量时并没有复制这些属性到变量上。

可以想象,每个对象都有自己的原型,可以随着继承关系一层层追溯上去,直到遇到以null为原型的对象。Javascript定义null没有任何原型,是Javascript的一个原生值,原型链的最后会链接向null。我永远单推null。

在ES6之后可以使用[Object.getPrototypeOf()]和[Object.setPrototypeOf()]访问,当然还有并不是标准但仍被广大浏览器实现的__proto__属性也能访问原型。标准是什么?能吃吗?

例如,Object.prototype指向Object的原型对象,即Object的构造函数。由于Object.prototype同时也是原型链的末尾,所以它的原型是null:

1
2
console.log(Object.prototype);   // 输出Object的构造函数
console.log(Object.prototype.__proto__); // 输出null

继承与原型链

之前提到可以用new Object()创建新的对象,它执行时做了这些事:

  1. 创建一个空白的对象,并继承Object.prototype
  2. 将创建的新对象作为this的上下文,使用指定的参数(这里没带任何参数)调用构造函数Objectnew Object等价于new Object()
  3. 如果构造函数return一个对象,这个对象就被作为new的结果。如果构造函数没有return,或是return了其他类型(比如null、number、bool之类),new默认将创建的对象作为结果

类似的,自己编写一个函数,用new就可以创建新的以这个构造函数为原型的对象实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function yousei() {
this.name = '⑨';
}

var cirno = new yousei();
console.log(cirno.name); // 输出 ⑨

var dai = new yousei();
console.log(dai.name); // 输出 ⑨

dai.name = 'daiyousei'; // 修改dai的name属性
console.log(cirno.name); // 输出 ⑨
console.log(dai.name); // 输出 daiyousei

yousei.prototype.location = 'kirinomizuumi'; // 向yousei.prototype添加属性
console.log(cirno.location); // 输出 kirinomizuumi
console.log(dai.location); // 输出 kirinomizuumi

可以看到,在构造函数中可以定义属性,这些属性可以通过new构造函数生成新的实例对象而被继承,新对象的原型就是这个构造函数。

新创建的对象可以设置各自的属性,互不影响,例如dai.name = 'daiyousei'不会影响到cirno.name,更不会影响到函数yousei中定义的属性name。继承的属性在创建时就由构造函数定义在新对象中,而不是链接到原型。

每个函数都有一个prototype属性,这个属性用于在创建新的实例对象时作为新对象的原型,和对象的原型(即对象的__proto__属性)不是一个概念。如果输出yousei.prototype就可以发现,yousei.prototype是一个包含构造函数yousei的对象,而且和cirno.__proto__一样,它只是函数yousei的一个属性,在创建dai时作为dai的原型对象。这和Object.prototype的含义不同,Object.prototype表示对象类型实例的原型对象。访问对象的原型应当使用Object.getPrototypeOf()或非标准的__proto__

1
2
var o = {};
console.log(o.__proto__ === Object.prototype); // 输出true

这也说明对象只是与原型链接,而不是将原型复制到自己的__proto__属性。所以随意对prototype属性赋值可能会导致原型链崩坏。

当然__proto__属性也不能随意修改,因为不是对象类型或者null是毫无影响的:

1
2
3
4
5
6
7
8
9
10
11
var o = {};
console.log(o.__proto__); // 输出o的原型对象

o.__proto__ = 'Misaka030';
console.log(o.__proto__); // 不受影响,输出o的原型对象

o.__proto__ = {};
console.log(o.__proto__); // 输出 {}

o.__proto__ = null;
console.log(o.__proto__); // 输出 undefined

yousei.prototype上可以添加新的属性,这些属性对于从yousei继承出去的对象来说是共享的,例如cirno.locationdai.location都能访问到yousei.prototype.location,而这就是通过原型链来实现的继承:

  1. 执行dai.location时会先在dai的属性中寻找location,dai的属性有{ dai: "daiyousei" },自然没有location属性
  2. 接下来会沿着原型链往上逐层寻找。dai的原型dai.__proto__有属性{ location: "kirinomizuumi" },这样就可以结束寻找返回属性的值了
  3. 如果继续顺着原型链,可以发现dai.__proto__.__proto__Object.prototype
  4. 继续,dai.__proto__.__proto__.__proto__是null,到这原型链就结束了。止まるんじゃねぇぞ

所以,由于属性是顺着原型链一个个对象寻找下去的,就有了下面这种情况:

1
2
3
4
5
6
7
8
9
function ningen() {
this.type = 'ningen';
}

var marisa = new ningen;
console.log(marisa.type); // 输出 ningen

ningen.prototype.type = 'youkai';
console.log(marisa.type); // 输出 ningen

编写构造函数可以让实例继承属性和方法,除了gettersetter。构造函数里得用this的,get和set根本没能写的地方啊?!这压根儿用不了getter和setter。

不过还有其他方法,不仅让这些特性都能用上,还能更好写更好看。

继承的其他方式

之前提到Object.create()也可以创建新的对象,它使用已有的对象作为新创建属性的原型对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var tengu = {
karasu: 'Himekaidou Hatate',
hakurou: 'Inubashiri Momiji',
get yome () { // getter和setter也能用啦
return this.hakurou; // this指向的是继承的实例,而不是原型对象
}
}

console.log(tengu.yome); // 输出 Inubashiri Momiji

var t = Object.create(tengu);
console.log(t.yome); // 输出 Inubashiri Momiji

t.hakurou = 'Syameimaru Aya';
console.log(t.yome); // 输出 Syameimaru Aya

需要注意的是this的上下文是创建的新对象。当然我永远喜欢犬走椛。

不过有个特殊情况,Object.create()可以创建没有原型的对象:

1
2
var o = Object.create(null);
console.log(o.hasOwnProperty); // 输出 undefined

ES6引入了class,可以通过新的关键词classconstructorstaticextendssuper来模拟oop的感觉。当然,这也支持getter和setter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Jinjya {
constructor (miko) {
this.miko = miko;
}
}

class Hakurei extends Jinjya {
constructor (miko) {
super(miko);
}
get addr () {
return 'Gensokyo';
}
}

var Moriya = new Hakurei('Kochiya Sanae');
console.log(Moriya.miko); // 输出 Kochiya Sanae
console.log(Moriya.addr); // 输出 Gensokyo

博麗神社 妖怪の山守矢支店

参考文献

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Basics
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain