《大前端三问》 - JavaScript中的面向对象与原型链1

JavaScript本身不提供一个 class 实现(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的), 而是一种基于原型的语言(prototype-based language) —— 同一类型的对象共有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)

我看过许多文章,一上来就大讲prototypethis原型链等等“高深”的内容,把各种概念抛出来把人看得云里雾里、似懂非懂。而在这里,我用自己的思路和大家一起分析与学习下JS的面向对象技术。我将以面向对象的三大特性为切入点,尝试着深入了解JavaSctipr的面向对象设计思路与实现:

  1. 抽象与封装;
  2. 继承;
  3. 多态。

然后在这个探索中,我们再来着重弄清楚这几个“高级”问题:

  1. JS的对象、函数是怎样的关系;
  2. 什么是原型(prototype),什么是原型链
    3.执行上下文this

本章中,我们将主要讨论JavaScript是如何实现面向对象编程的封装的特性。

1 抽象与封装

我们说,封装主要做的是两件事:抽象复用

抽象是一种思想,讲究的是如何把现实世界映射到计算机世界中。而封装是计算机中的实现方式,通过设计一套组合方式把基础的数据结构装配成为抽象出来的整体。

要实现这两个概念,面向对象这套编程体系定义了对象这两种结构。其中,对象是一个个具体的客观实体,而是这些实体共有特征的抽象。比如,我们每个人都是具体的实体、是一个个对象;而我们人又具有共同的特性 – 都有名字、性别、都会说话等。基本的抽象模型如下:

类与对象伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Class People {
String name; // 姓名
int age; // 年龄

// 构造函数
People(name, age) {
this.name = name;
this.age = age;
}

sayHi() {
log('Hello,my name is ' + this.name);
}
}

People me = new People('Chauncey', 30);
People peter = new People('Peter', 20);

像上面的例子虽然是伪代码,但是很好理解。这里我定义了一个抽象的类 – People表示人; 然后使用类的构造函数又定义了具体的2个对象 – mepeter,该定义的方法是在构造函数前加new关键字 – new People('Chauncey', 30)

  • 通过抽象可以将客观世界映射到计算机世界中,这种思想为我们对程序设计提供了思想的基础。
  • 而通过封装,我们把个体及他们的属性组合了起来。
    比如说我现在要构建一个学生管理系统,抽象必然是我考虑的第一步。

1.1 抽象与封装在JavaScript中的实现

那么,在JavaScript中如何实现抽象的呢?具体来说,就是在JavaScript的世界中,对象是怎样表现的,又是怎样表现的。

我们先从对象入手来看:

1.2 从对象入手

首先开门见山,JavaScript是一种基于原型的语言,并没有类的概念。所以在JS中,只有对象,而对象其实就是一个散列表结构!

JavaScript中的对象,Object,其实可以简单理解成“名称-值”对(而不是键值对:现在,ES 2015 的映射表(Map),比对象更接近键值对),不难联想 JavaScript 中的对象与下面这些概念类似:

  • Python 中的字典(Dictionary)
  • Perl 和 Ruby 中的散列/哈希(Hash)
  • C/C++ 中的散列表(Hash table)
  • Java 中的散列映射表(HashMap)
  • PHP 中的关联数组(Associative array)

这样的设计让JavaScript的对象简单灵活,能应付各类复杂需求。正因为 JavaScript 中的一切(除了核心类型,core object)都是对象,所以 JavaScript 程序必然与大量的散列表查找操作有着千丝万缕的联系,因为散列表擅长的正是高速查找。

js中,可以直接使用字面的方式定义一个对象,就像定义一个散列表那么简单。比如下面的代码:

JavaScript直接使用字面的方式定义一个对象
1
2
3
4
5
6
7
let me = {
name: 'Chauncey',
age: 31,
sayHi: function () {
console.log('Hello, my name is ' + this.name);
}
}

像上面这样,我就定义了一个对象,而我们知道,JS中的对象其实是个散列表结构:

上面代码中对象的结构
1
2
3
4
5
6
7
8
9
┌───────────────────────┐     ┌──────────────────────────┐
│ me │ ┌─▶│ function sayHi() │
├────────────┬──────────┤ │ ├──────┬───────────────────┤
│ name │'Chauncey'│ │ │ f │ console.log(...) │
├────────────┼──────────┤ │ └──────┴───────────────────┘
│ age │ 31 │ │
├────────────┼──────────┤ │
│ sayHi() │ ─────┼──┘
└────────────┴──────────┘

上面的代码在内存中就是这样的一个结构。这样的设计很好实现,也就是当我们定义一个对象时,JS会自动为我们开辟一个散列表空间,其中表的key是对象成员的名字,而成员的值有两种:值类型引用类型,如果成员是一个基本类型(比如是数值),则直接存其值,这是值类型。如果成员变量是指向一个方法或对象,则为引用类型,就像C中的指针一样,它存的是一个指向所指方法或对象的引用

然后我们看到me这个对象的sayHi成员函数引用,指向的是一个sayHi()方法,在这里不得不提一个就是javaScript中的方法与其它语言中的方法并不相同。

1.3 初探javaScript中函数与对象的关系

在JavaScript中,函数与对象的关系是比较暧昧而复杂的,后面我们会详细介绍。但在这里,我们会大概讲一下JS中的函数,暂且不必太深入,我们先不求甚解地探索下。

1.3.1 函数也是一个对象

首先,在JavaScript中,函数也是一个对象。这从我上面画的图可以看出,函数实际上也是一个散列表的组成结构。这就和我们一般熟悉的编译型语言的方法很不同了。众所周知,在编译型语言中,方法是程序的重要组成结构,我们说进程就是运行着的程序。在编译型语言中,方法会被编译成一条条命令,然后放在只读的一块内存中,我们称之为代码段。

而在JS中,函数是一个对象,也是一个散列表结构,而其内一条条的语句,实际上就是当作一条字符串,就像上图一样,我用f表示函数的代码部分,对应的值是具体的代码。为什么可以这样?因为JavaScript是解释性语言,要运行一个方法只需要将之放入解释器就行,这就使其非常的灵活。

我们可以做个实验,如下代码示例:

下面两个定义函数的方法是一样的
1
2
3
4
5
6
7
8
9
10
11
function f1(x, y) {
x++;
return x + y;
}

// 等价于 ==>

let fn2 = new Function('x', 'y', 'x++; return x + y');

console.log(fn1(1, 2)); // 4
console.log(fn2(1, 2)); // 4 ==> 两个函数输出是一样的!

这里我们就看出,JS中声明一个方法其实就是定义了一个方法对象,而具体的方法体就是一段由JS表达式语句组成的字符串。

还有一个就是 –在JavaScript中,函数也是一等公民

1.3.2 函数是一等公民

一等公民”这个称呼起源于哪里已不好追究了,但他的意思主要就是,在JavaScript中,函数也是一个对象,而且是重要的一种一等对象。这样设计的好处在于,你可以将函数像普通对象一样传递(作为另一个函数的参数、作为函数的返回值,或者将之随便赋值给一个变量)。

比如在JS中你可以随便定义一个函数:

1
2
3
function sayHi() {
return 'Hi~';
}

你可以像定义一个字面对象一样给他添加成员变量、成员方法(这里只是意思上的区分,其实我们知道对象的散列表本质,成员变量或成员方法也好都只是添加一条key-value记录)。

1
2
3
4
sayHi.toWho = 'Peter';
sayHi.hello = function (toWho) { console.log('Hello, ' + toWho); }

sayHi.hello(sayHi.toWho); // output => Hello, Peter

这样一看,JS中function的特性和的要求很像,都可以指定成员与方法。而正因为函数的这个特性,让它成为了JavaScript面向对象编程中用于实现封装的主要工具。

1.3.3 使用函数实现类的封装

因为在JS中,函数被设计的无比强大,以至于设计者将面向对象的封装“重任”都交之于它(js语言的早期设计是一种极简风格,追求对关键字能省则省)。我们上小节说过,JS的function和高级面向对象语言的class很像,都可以指定成员与方法,于是可以用之来这样实现我们上面对“People”的封装:

用函数实现类的封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function People(name, age) {
this.name = name;
this.age = age;

this.sayHi = function() {
console.log('Hi~My name is ' + this.name);
}
}

let me = new People('Chauncey', 30);
let peter = new People('Peter', 20);

me.sayHi(); // Hi~My name is Chauncey
peter.sayHi(); // Hi~My name is Peter

上面的代码其实很好懂,语义上就不作过多解释。有趣的是,在我们之前伪代码以及多数语言中,都是使用Class关键词来定义一个类,而在JS中索性就用function来定义了。这表现了JS极简的风格,当然,到ES6中还是引入了Class关键字,但那只是语法糖,JS并没有为之增加一个Class类型。

虽然经过我们上面的学习,这个例子已很容易理解了 –这就是JS实现的抽象与封装,用函数来实现类型的封装,定义对象实例们的通用属性与方法。基于构造函数生成具体的对象实例

所以,至此为止,我们已经掌握了在JavsScript中是如何实现面向对象的抽象与封装的。

但是这里我们还有会产生好奇,在JS中,new function()会发生什么事,对象是怎样基于函数生成的?这就涉及抽象与封装的另一个重要概念 – 复用

2 复用

我们现在抛开上面关于抽象与封装的概念想一想,为什么要有设计“类”与“对象”这两层数据结构? – 答案就是复用

因为在实际的操作中,我们操作的其实都是一个个具体的对象,而之所以抽象出“类”这个概念,就是为了复用。让具体的单个对象可以省去许多一样的模板代码。比如上面的People的例子,我可以每个对象都直接使用字面定义方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let me = {
name: 'Chauncey',
age: 31,
sayHi: function () {
console.log('Hello, my name is ' + this.name);
}
}

let Peter = {
name: 'Peter',
age: 20,
sayHi: function () {
console.log('Hello, my name is ' + this.name);
}
}

但是这样明显会出现许多重复的定义,而且当我想去对它们的共同结构作修改时,我不得不一个个地去修改个体对象的代码。因此,我们说 – 类的存在主要是为了代码复用。(注意这里复用有两层意义:一个是减少重复代码,一个是共用相同逻辑)

2.1 C++中类与对象的实现

我们来看看一般编译型语言中类与对象的实现。我这里用结构体举例而不是使用通用的高级语言的Class是因为一般大家都对struct的内存布局更加熟悉、其底层的理解更加简单。我们看一个例子:

1
2
3
4
5
6
7
8
9
struct People {
char name[10];
int age;
void sayHi() {
// ...
}
};
People *me = (struct People *)malloc(sizeof(struct People)));
People *peter = (struct People *)malloc(sizeof(struct People));

这里定义的People与上面我们用JS作的定义效果是一样的。我们都知道,结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存(正因为这个概念,所以在某些语言中,我们也常将对象称之为实例instance)。所以我们发现在编译语言的实现中,类(比如上面示例中的struct),更像是一个模板。而根据这个模板,生成的对象实例(结构体实例)在内存中表示为:

1
2
3
4
5
6
7
8
9
        ┌──────────┐
│ me │
├──────────┤
name │'Chauncey'│
├──────────┤
age │ 31 │
├──────────┤
sayHi() │ 0x???? │ 函数在代码段中的地址
└──────────┘

我们看到,在C++中,实例在内存中的表示与在JS中大不相同。在JS中,变量是使用散列表存储,根据key来快速寻址成员变量value。而C++中成员是使用偏移地址来表示的(比如说你要访问name变量,编译器会根据name变量类型算出其大小与相对对象首地址的偏移),这样的好处是其对象的结构更简单了,寻址也会较散列的形式都快些。但是缺点也很明显,就是类结构与实例结构是定死的,因为使用偏移寻址,后期想动态添加成员根本不可能。

在对比中学习可以让我们对一门学问了解的都深刻,现在我们知道,在C++中,复用是使用模板的形式来实现的,对象们共用了一份相同的内存布局,这样可以达到更高效率的操作,但是有失灵活性。

2.2 原型prototype

JavaScript中对复用的实现用了另外一种思路,它抽象出来了一个共用的底层对象 – prototype(我们常翻译为“原型”)。由这个底层对象来存放对象共性的东西。这样做是为什么呢?

比如我们探研下之前的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function People(name, age) {
this.name = name;
this.age = age;

this.sayHi = function() {
console.log('Hi~My name is ' + this.name);
}
}

let me = new People('Chauncey', 30);
let peter = new People('Peter', 20);

console.log(me.sayHi === peter.sayHi); // output=> false

这里多说一下,咱的示例代码都可以简单地使用chrome的调试工具来测试,在最后的《小结-使用chrome调试js代码》一节中我们将附操作方法。

从上面的代码我们发现,两个对象的内部函数居然不是同一个。这当然啦,因为new People时, function内的代码都会执行一把,在这里我们发现this.sayHi相当于都重新赋值了一遍,也就是每创建一个对象都生成了一个sayHi()方法。

这里我们不得不又先抛出一个概念,我们new People(...)时究竟是在干嘛?

2.2.1 所有函数都可以是构造函数

我们现在细看一个People这个函数。有没有发现它和java或其它高级面向对象语言的中构造函数很像!没错,JavaScript就是这么精简!在JS中,只要你对函数使用new语句,就可以将此函数变成构造函数(注意,ES6中新增的键头函数不可作为构造函数,只能作为普通函数)。

为什么我要强调这个特性呢?因为JavaScript是这么精简,以至于我们要为之自己脑补许多代码。。。我们将上面People的代码脑补一下:

脑补js构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Class People {
name;
age;
sayHi;

function People(name, age) {
this.name = name;
this.age = age;

this.sayHi = function() {
console.log('Hi~My name is ' + this.name);
}
}
}

这下对于new People()我们就好理解了,当我们对函数作new时,js会将之待作构造函数,并返回为我们生成好的对象。这点和高级面向对象语言比如Java是一样的,在Java中,new ClassName(param)也是调用该类的构造函数生成对象,并完成成员的初始化。

而在JS中并没有class的概念,但你可以更直接地将function“升级”为构造函数。使用new操作符和构造器函数结合来创建对象。我们现在翻回去看看People这个构造函数的定义,在里面this就代指你生成的对象,我们通过给this也就是生成的对象赋于同一的属性与方法,来完成对象的初始化。这样生成出来的对象都有相同的属性与方法,所以JS创造者大概可以说,“你看,我们没有Class关键字,面向对象的抽象与封装不是也实现的好好的吗”。

这样一来你就可以发现什么了吧,在构造函数function People(name, age)中我们对每个对象的sayhi方法都作了这样的赋值 – this.sayHi = function(),这就是每个对象的sayHi方法都不相同的原因(他们都创建了一份新的实例)。那我们怎样解决这个问题呢?JS的解决方案就是用所有对象共享一个公共的对象结构,把公用的属性与方法放在此公共结构上 – 这个结构就是prototype(原型)

现在我们把话题转回来。说回prototype

2.2.2 prototype__proto__与原型链

首先要注意下,经过上一节我们的脑补,我们知道早期JavaScript是一种极为精简的语言,其能省就省的风格造就了它连class关键字都懒得定义。但我们还是要清楚,这后面我们所说的function,其实大多数指的是面向对象中说的class的概念

我们现在的疑问是,所有对象共享的这个公共对象结构 – prototype – 是在何时生成的 – 答案是在函数定义的时候。每当我们使用function定义一个函数时(注意,新增的键头函数不会生成prototype),解释器会自动地为函数生成一个prototype对象(注意,prototype也是一个对象),并为该function增加一个成员属性prototype指向这个对象。

现在,我们就可以将公共的属性或方法定义在它的prototype对象上,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function People(name, age) {
this.name = name;
this.age = age;
this.sex = 1;
}
People.prototype.sayHi = function() { // <--- 注意这里
console.log('Hi~My name is ' + this.name);
}
People.hello = function() {
console.log('Hello');
}
People.Male = 1;
People.Female = 0;

let me = new People('Chauncey', 30);
me.sayHi(); // output=> Hi~My name is Chauncey
// me.hello(); 报错
People.hello(); // output=> 'hello'
me.sex = People.Male;
console.log('The sex of me is ' + ((me.sex === People.Male)? 'Male': 'Female'));
// output=> The sex of me is Male

现在,整个对象与函数的关系如下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌───────────────────────┐
│ function People() │◀───────────────────────────────────────┐
├────────────┬──────────┤ ┌──────────────────────────┐ │
│ prototype │ ─────┼─────┐ │ People │ │
├────────────┼──────────┤ ├───▶│ <<prototype>> │ │
│ Male │ 1 │ │ ├────────────┬─────────────┤ │
├────────────┼──────────┤ │ │constructor │ ────────┼──┘
│ Female │ 0 │ │ ├────────────┼─────────────┤
├────────────┼──────────┤ │ │ __proto__ │ Object │
│ hello │(function)│ │ │ │<<prototype>>│
└────────────┴──────────┘ │ ├────────────┼─────────────┤
| │ │ sayHi │ (function) │
me = new People() | │ └────────────┴─────────────┘
▼ │
┌───────────────────────┐ │
│ me │ │
├────────────┬──────────┤ │
│ __proto__ │ ─────┼─────┘
├────────────┼──────────┤
│ name │'Chauncey'│
├────────────┼──────────┤
│ age │ 31 │
├────────────┼──────────┤
│ sex │ 1 │
└────────────┴──────────┘

我们怎么验证此图的正确性呢?按《小结》里的方法在chrome调试器里输入上述示例代码,将`People`与`me`都打印出来就知道了。

为什么调用me.sayHi()可以找到正确的方法呢?这里又有一个重要概念,就是当使用new生成JS对象时,解释器会自动为之添加一个__proto__成员,其指向People的原型People.prototype!!

因此,当你执行:

1
var me = new People('Chauncey', 31);

JavaScript 实际上执行的是:

1
2
3
var me = new Object();
People.call(me, 'Chauncey', 31);
me.__proto__ = People.prototype;
  • 原型链

而当我们访问对象的成员时,解释器会自动帮我们作向上查找,当在本对象的散列表结构中没有找到相应的引用时,会沿着其__proto__属性找到我们所说的公共对象prototype,到其原型对象中去找!这有点像Objective-C中的消息转发机制。

这种机制实现了JS中的复用特性,现在你可以想想看,无论我用People定义多少个对象,他们都可以共用一个prototype对象。prototype是一个可以被所有实例对象共享的对象,称之为原型。它是一个名叫原型链(prototype chain)的查询链的一部分。当访问一个对象(比如People的实例me)的属性或方法时,会先在此对象已定义的成员中找,如果没找到,就会到其prototype中去找!没错,这看起来有点像继承关系的到父类中去找一样(比如sayHi这个函数,我们先在me这个对象的散列表中找。结果发现没找到,就顺着其__prop__属性找到它的原型People.prototype,最终找到sayHi()方法并调用之)。

然后我们还要注意一点的是,me.hello();失败了,也就是在对象中直接调用模板函数的方法是行不通的,因为变量的查找只会沿着__proto__指向往上查找。模板函数上直接定义的方法只能通过模板函数调用,这也是很多高级语言中实现的所谓实例变量(在构造器内使用this定义的变量成员)与静态变量(在函数上直接定义的变量成员)。

2.2.3 关于prototype的oneMoreThing

我们注意到People.prototype中有个constructor指回了People函数。这种设计固然给从原型出发找到其相应的模板函数提供了路径,另一方面,这种类似“循环引用”的设计也给人以原型与模板函数一一绑定的感觉。

还有一点是我们注意到我把People.prototype__proto__属性也画了出来。那当然咯,因为原型也是对象,是对象就有__proto__属性。而People.prototype__proto__指向的是Object.prototype。这是什么意思呢?

这说明所有原型都是解释器自动地使用new Object()生成出来的~也说明了当对象的属性在原型上也找不到时,还会继承向上查找,一直找到Object原型上去!!这不符合面向对象的思想吗?在面向对象的世界中,一切对象皆是Object。我们不妨作如下验证:

1
2
3
console.log(People.prototype.__proto__ === Object.prototype); // ture
console.log(me.__proto__.__proto__ === Object.prototype); // true
console.log(me.toString()); // [object Object]

这里给出了验证,而且me.toString()的调用成功也说明了一点,就是JS的成员查找可以顺着__proto__原型链一直往上找,使得Object的方法也可以为子类们享用。

好了,大家看到这里,想必已经对JS的面向对象编程有了个大概的了解。因为抽象和封装是面向对象的基础,我们今天了解了其抽象和封装的实现,也就是掌握了基oop的基础。这为我们接下来的学习也奠定了根基,下章我们将一起学习另一个特性,也就是继承在JS中是如何实现的。

这里咱们来小结一下:

3 小结

3.1 内容小结

  1. JavaScript是基于原型的面向对象语言,并没有Class的概念。面向对象的抽象与封装的实现主要是由objectfunctionprototype三种主要结构来构建。
  2. 其中object指的是具体的对象实例,本质上是一个散列表结构;function本质上是构造函数,但在其内部实现了为所生成的对象定义相同属性与方法的抽象作用;而prototype原型的出现是为了解决面向对象的复用问题,我们说这里复用有两层意义:一个是减少重复代码,一个是共用相同逻辑。
  3. JS中函数是一个对象,也是一等公民,可以自由赋值、自由传递(作为方法的参数或返回值等)。
  4. JS中没有class的概念,你可以更直接地将function“升级”为构造函数。使用new操作符和构造器函数结合来创建对象 – new People()
  5. prototype原型是伴随着function构造函数的定义而自动产生的,使用func.prototypeobj.__prop__可以直接访问到。
  6. 而正因为__prop__的存在,产生了所谓的原型链。当我们访问对象的成员时,如果在本对象的散列表结构中没有找到相应的引用时,解释器会自动帮我们作向上查找,沿着其__proto__属性找到原型对象prototype。如果还是没有,会一直沿着原型的原型一路向上找,直到找到Object.prototype, 其__proto__为null。

3.2 使用chrome调试js代码

随便打开chrome,按下alt + cmd + j可以打开调试模式。

选中consoletab,这个就是调试工具的调试终端。比如我们在里面输入如下代码(使用):

1
2
3
function test() {
console.log('--')
}

然后你就可以输入test, test.prototype去查看对象的组成。比如我们查看test.prototype,就可以看到输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
test.prototype
{constructor: ƒ}
constructor: ƒ test()
arguments: null
caller: null
length: 0
name: "test"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: VM30968:1
[[Scopes]]: Scopes[2]
__proto__:
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()

3.3 本章相关命令与方法

js中,可以直接使用字面的方式定义一个对象,就像定义一个散列表那么简单。下面罗列了javaScript中创建对象的几个方法:

  1. 使用new关键字;
  2. 创建字面值对象;
  3. 使用Object.create(obj)基于现有对象创建对象
对象相关函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建
let obj = new Object();
let obj2 = {}; // 与上面的创建一个对象相同
var obj3 = Object.create(obj1); // 基于现有对象创建对象
// 定义一个复杂的对象
let obj = {
name: "Carrot",
details: {
color: "orange",
size: 12
},
sayHi: function (params) {
console.log('Hi~');
}
};
// 对象的属性可以通过链式(chain)表示方法进行访问:
obj.details.color; // orange
obj["details"]["size"]; // 12

3.4 JS面向对象的抽象与封装的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function People(name, age) {
this.name = name;
this.age = age;
this.sex = 1;
}
People.prototype.sayHi = function() {
console.log('Hi~My name is ' + this.name);
}
People.hello = function() {
console.log('Hello');
}
People.Male = 1;
People.Female = 0;

let me = new People('Chauncey', 30);
me.sayHi(); // Hi~My name is Chauncey
// me.hello(); 报错
People.hello(); // 'hello'
me.sex = People.Male;
console.log('The sex of me is ' + ((me.sex === People.Male)? 'Male': 'Female')); // 'The sex of me is Male'

4 引用

  1. 《重新介绍JavaScript》- 介绍JS很好的入门材料
  2. 《JavaScript高级程序设计》- (美)(Nicholas C.Zakas)扎卡斯 学习JS挺好的入门教材
  3. 《继承与原型链》- MDN web docs 继承与原型链, 很好,讲了性能,和原型实际
  4. [《Function函数与Object对象的关系》- 网文](https://www.cnblogs.com/nature-tao/p/9504712.html Function函数与Object对象的关系的一篇挺好的探索文章