《大前端三问》 - JS中的对象与原型

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

在本章中,我们就要弄清楚这几个问题:

  1. JS的面向对象是怎样的;
  2. 什么是原型,什么是原型链

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

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

这样的数据结构设计合理,能应付各类复杂需求,所以被各类编程语言广泛采用。正因为 JavaScript 中的一切(除了核心类型,core object)都是对象,所以 JavaScript 程序必然与大量的散列表查找操作有着千丝万缕的联系,而散列表擅长的正是高速查找。

1 类、对象与函数

首先,一个熟悉面向对象编译的程序员一开始肯定会想,用JS怎样来定义一个类,然后实例化一个对象。但是你会发现,JS中定义一个类居然是:

定义一个Person类
1
2
3
4
5
6
7
8
9
10
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = function() {
return this.first + ' ' + this.last;
}
this.fullNameReversed = function() {
return this.last + ', ' + this.first;
}
}

这不是定义一个函数吗?

要解决这个疑惑,我们先要来了解下JavaScipt中的函数。在javascript中,函数可以有属性有方法的(就像上面Person的例子一样)。

打印方法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
26
27
function doSomething(){}
console.log(doSomething.prototype);
// >> output:
// constructor: ƒ doSomething()
// arguments: null
// caller: null
// length: 0
// name: "doSomething"
// prototype: {constructor: ƒ}
// __proto__: ƒ ()
// [[FunctionLocation]]: VM280:1
// [[Scopes]]: Scopes[1]
// __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__()

2 对象

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

创建对象有3种方法:

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

因此,当你执行:

var o = new Foo();
JavaScript 实际上执行的是:

var o = new Object();
o.proto = Foo.prototype;
Foo.call(o);

2.1 对象与原型

我们之前说过JavaScript通过原型链而不是类来支持面向对象编程。下面的下面的例子创建了一个对象原型,Person和这个原型的实例Peter

创建一个对象原型
1
2
3
4
5
6
7
8
9
function Person(name, age) {
this.name = name;
this.age = age;
}
// 定义一个对象
let Peter = new Person("Peter", 24);
// 我们创建了一个新的 Person,名称是 "Peter"
// ("Peter" 是第一个参数, 24 是第二个参数..)

2.2 原型链与继承

2.2.1 原型

我们先直接看下原型方法的发展

原型方法的发展
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
28
29
30
31
32
33
34
35
36
37
// 实现1
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = function() {
return this.first + ' ' + this.last;
}
this.fullNameReversed = function() {
return this.last + ', ' + this.first;
}
}
// 实现2
function personFullName() {
return this.first + ' ' + this.last;
}
function personFullNameReversed() {
return this.last + ', ' + this.first;
}
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = personFullName;
this.fullNameReversed = personFullNameReversed;
}
// 实现3
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.fullName = function() {
return this.first + ' ' + this.last;
}
Person.prototype.fullNameReversed = function() {
return this.last + ', ' + this.first;
}

实现1中,每个实例对象都有相同的一份方法,这样做显然是浪费的。所以我们在实例中将方法抽象出来,作为全局方法,这样虽然没有造成浪费,但是全局方法作用域还是太大了。因此在js中,我们引入了prototype原型 的概念。

prototype是一个可以被所有实例对象共享的对象,称之为原型。它是一个名叫原型链(prototype chain)的查询链的一部分。当访问一个对象(比如Person的实例peter)的属性或方法时,会先在此对象已定义的成员中找,如果没找到,就会到其prototype中去找!没错,这看起来有点像继承关系的到父类中去找一样。

我们不仅可以给一个类添加原型方法,甚至可以给内置数据类型添加原型方法:

给内置数据类型添加原型方法
1
2
3
4
5
6
7
8
9
String.prototype.reversed = function() {
var r = "";
for (var i = this.length - 1; i >= 0; i--) {
r += this[i];
}
return r;
}
'Abc'.reversed(); // 'cbA'

像上面一样的操作,可以给系统的内置类型添加属性与方法。

3 this

我们说对象原型,在JavaScript中,函数也是对象,而函数与对象中的this关键字,大有讲究。

this,当使用在函数中,代指调用了当前函数的对象!如果在对象上使用点或方括号来访问属性或方法,其函数中的this就是此对象。

this在函数中的指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function makePerson(first, last) {
return {
first: first,
last: last,
fullName: function() {
return this.first + ' ' + this.last;
},
fullNameReversed: function() {
return this.last + ', ' + this.first;
}
}
}
let simon = makePerson("Simon", "Willison");
simon.fullName(); // Simon Willison
simon.fullNameReversed(); // Willison, Simon
let fullname = simon.fullName;
fullname(); // undefined undefined

注意上面示例的后面,我们将对象的方法摘出来,在全局中调用之。此时,this指向全局对象(如果是在浏览器中就是window)。此时因为全局对象并没有first与last属性,所以输出两个undefined。

4 小节

本单相关函数

本单相关函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// -- 创建 --
let obj = new Object();
let obj = {}; // 与上面的创建一个对象相同
// 定义一个复杂的对象
let obj = {
name: "Carrot",
"_for": "Max", //'for' 是保留字之一,使用'_for'代替
details: {
color: "orange",
size: 12
}
}
// 对象的属性可以通过链式(chain)表示方法进行访问:
obj.details.color; // orange
obj["details"]["size"]; // 12
// -- 对象原型 --
function Person(name, age) {
this.name = name;
this.age = age;
}
// 定义一个对象
let Peter = new Person("Peter", 24);

5 引用

  1. 《重新介绍JavaScript》- 介绍JS很好的入门材料

继承与原型链, 很好,讲了性能,和原型实际
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

JS的继承
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Object_prototypes
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Inheritance

Function函数与Object对象的关系
https://www.cnblogs.com/nature-tao/p/9504712.html

坚持原创技术分享,您的支持将鼓励我继续创作!