【道生万物】理解Javascript原型链
道生一,一生二,二生三,三生万物。万物负阴而抱阳,冲气以为和。
-- 老子 《道德经》
道:空值
理解Javascript(以下简称“JS”)之道,需要先理解undefined
与null
的区别。
在JS中,undefined
是全局对象的一个属性,它的初始值就是原始数据类型undefined
,并且无法被配置,也无法被改变。undefined
从字面意思上理解为“未定义”,即表示一个变量没有定义其值。
而null
是一个JS字面量,表示空值,即没有对象。与undefined
相比,null
被认为是“期望一个对象,但是不引用任何对象的值”,而undefined
是纯粹的“没有值”。
null
是对象原型链的终点,其值既有(是一个对象)又无(不引用任何对象),代表着对象本源的一种混沌、虚无的状态,正与老子《道德经》中的“道”,有着同等的意义。
// null为对象原型链的终点
console.log(Object.getPrototypeOf(Object.prototype)); // null
// null是一个对象
console.log(typeof null); // object
// null 为空
console.log(!null); // true
道生一:原型
JS中的所有事物都是对象:字符串、数字、数组、日期,等等。在JS中,对象是拥有属性和方法的数据。
为了描述这些事物,JS便有了“原型(prototype)”的概念。
显式原型(explicit prototype property) 每一个函数在创建之后都会拥有一个名为prototype
的属性,这个属性指向函数的原型对象。用来实现基于原型的继承与属性的共享。
隐式原型 (implicit prototype link) JS中任意对象都有一个内置属性__proto__
(部分浏览器为[[prototype]]
),指向创建这个对象的函数(即构造函数)constructor
的prototype
。用来构成原型链,同样用于实现基于原型的继承。
对象的原型Object.prototype
用来描述最基本的对象。万物皆对象,所有的对象均具有隐式原型__proto__
,对象的原型也不例外。因为它生于虚无,所以它的__proto__
属性指向null
,即原型链的最顶端。而该原型,就是JS中万物之始。
一生二:对象与函数
拥有了描述事物的能力,却没有创造事物的能力,显然是不完整的,因此需要一个Object
的生成器来进行对象的生成。
JS将生成器以构造函数constructor
来表示,构造函数是一个指针,指向了一个函数。
函数(function) 函数是指一段在一起的、可以做某一件事的程序。构造函数是一种创建对象时使用的特殊函数。
对象的构造函数function Object
同时也是一个对象,因此需要一个能够描述该对象的原型,该原型便是Function.prototype
,函数的原型用来描述所有的函数。对象的构造函数的__proto__
指向该原型。
函数的原型本身也是对象,因此其__proto__
指向了对象的原型。同样,该对象也需要一个对应的生成器,即其构造函数function Function
。
函数的构造函数是由函数生成的一个对象,所以其原型即为函数的原型,其隐式原型也同样为函数的原型Function.prototype
。
instanceof
instanceof
操作符的内部实现机制和隐式原型、显式原型有直接的关系。instanceof
的左值一般是一个对象,右值一般是一个构造函数,用来判断左值是否是右值的实例。它的实现原理是沿着左值的__proto__
一直寻找到原型链的末端,直到其等于右值的prototype
为止。
根据上图展示的Object
和Function
的继承依赖关系,我们可以通过instanceof
操作符来看一下Object
和Function
的关系:
console.log(Object instanceof Object); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function instanceof Function); // true
函数与对象相互依存,分别定义了事物的描述方法和事物的生成方法,在生成JS万物的过程中缺一不可。
二生三:类
类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。
在ECMAScript 2015 中引入的JS类(classes)之前,要在JS中实现类便是采用原型继承的方式。
当把一个函数作为构造函数,使用new
关键字来创建对象时,便可以把该函数看作是一个类,创建出来的对象则是该类的实例,其隐式原型__proto__
指向的是该构造函数的原型。
在访问该对象的属性或方法时,JS会先搜索该对象中是否定义了该属性或方法,若没有定义,则会回溯到其__proto__
指向的原型对象去搜索,若仍然未搜索到,则会继续回溯该原型的原型,直到搜索到原型链的终点null
;
这种特性可以理解为:构造函数生成的实例,继承于该构造函数的原型。
得益于这种特性,我们可以使用定义构造函数的方式来定义类。
function Person() {} // 定义Person构造函数
// 通常以大写字母开头来定义类名
console.log(new Person() instanceof Person); // true
以上定义了Person
类,该构造函数是由Function
构造而来,所以其隐式原型指向函数的原型,而为了描述该事物,同时生成了该类的原型Person.prototype
,该原型又是由Object
构造而来,所以其隐式原型指向了对象的原型。
三生万物:实例
通过定义不同的类,可以对万物进行描述。而每个类对应的构造函数,即为万物的生成器。通过new
构造函数,可以得到类对应的实例,该实例可以理解为实实在在的事物,而不同于前述函数和对象这种抽象的定义了。
上图中new Object()
和new Person()
分别为通过基本对象类和Person
类的构造函数生成的实例,它们的隐式原型分别指向其构造函数的原型。因为Person
的原型的隐式原型指向了Object
的原型,所以new Person()
也同样是Object
的实例。
function Person () {} // 定义Person类
const obj = new Object(); // 实例化基本对象
const person = new Person(); // 实例化person对象
console.log(obj instanceof Object); // true
console.log(person instanceof Person); // true
console.log(person instanceof Object); // true
定义属性
对于以上定义的Person
类,我们希望能够更加详细地去描述它。
比如人应该拥有姓名、性别、年龄等属性,可以将这些属性在示例的构造过程中给其赋上。
function Person (name, gender, age) { // 定义Person类,并指定动态的属性参数
this.name = name;
this.gender = gender;
this.age = age; // 通过上下文中的this对象来为实例绑定属性
}
const person = new Person('Alice', 'female', 18);
console.log(person.name, person.gender, person.age); // Alice, female, 18
this
this
是JS中的一个关键字,代表了函数运行时生成的一个内部对象,该对象通常指的是调用该函数的那个对象。该对象只能在函数中使用。如果该函数是以构造函数的方式被调用,则this
指的是通过该构造函数实例化的实例对象。
定义方法
在给人定义了姓名等属性后,还需要对其进行一些描述来描述人应该拥有的行为。行为可以被抽象成函数,所以要对Person
定义一些类型为函数的属性,我们称之为“方法”。
function Person (name, gender, age, eat, run) { // 定义Person类,并指定属性和方法参数
this.name = name;
this.gender = gender;
this.age = age;
this.eat = eat;
this.run = run;
}
const alice = new Person('Alice', 'female', 18, function () {
console.log(`${this.name} eat food.`);
}, function () {
console.log(`${this.name} run.`);
});
alice.eat(); // Alice eat food.
alice.run(); // Alice run.
我们通过将函数作为构造函数参数的传递进去的方式,给实例添加了eat
和run
方法,在调用这些方法时,这些方法内部的this
指向的是调用其的对象,即为alice
,所以this.name
得到的是我们给alice
定义的姓名Alice
。
公共属性&方法
通过上述方式定义方法,必须在实例化一个对象的时候来定义方法的实现,这样显然不是很可取。大多数情况下,我们需要定义的方法应该是该类的所有实例都共用的方法。比如所有的人都要吃东西,可以跑步。同样,会有一些属性也是公共的,比如人都有拥有大脑,而大脑不同于姓名,大脑的结构不会因为不同的人而不同。
通过给该构造函数的原型添加方法,这些方法定义在原型中,所有实例对象都是原型的继承,同样会将原型的属性和方法继承下来。
function Person (name) { // 定义人类
this.name = name;
}
Person.prototype.eat = function () { // 给原型添加方法
console.log(`${this.name} eat food.`);
};
Person.prototype.think = function () {
if(this.hasBrain) {
console.log(`${this.name} think.`);
} else {
console.log(`${this.name} has no brain!`);
}
}
Person.prototype.hasBrain = true; // 给原型添加大脑属性
const alice = new Person('Alice');
alice.eat(); // Alice eat food.
alice.think(); // Alice think.
至此,已经可以用类来完整地描述一个事物了。
结语
万物皆实例,实例又都是由类来进行描述,而实例的属性即为对象,实例的方法即为函数,由此可见,万物都是由对象和函数相依相生来进行定义的。而对象又定义了函数,函数又构造了对象,这种关系又由“原型”的方式进行链接,组成了JS的多态世界。
JS之道,万物自成一体,理解了这个道理,掌握JS的原型便不成问题。
附:对引言的理解
因一切事物非事物,不约而同,统一遵循某种东西,无有例外。它即变化之本,不生不灭,无形无象,无始无终,无所不包,其大无外,其小无内,过而变之、亘古不变。其始无名,故古人强名曰:道。道生一,是从无到有,生一气,气分阴阳,为二,阴阳和合出变化,为三,由变化而生万物。阴阳二气互相冲突交和而成为均匀和谐状态,从而形成新的统一体。