我们在讨 (mian) 论(shi)JavaScript 这门语言时, 总是绕不过的一个话题就是继承与原型链那么继承与原型链到底是什么呢?
我很喜欢的一个聊天模式是: 我不能说 XX 是什么, 我只能说 XX 像什么也就是说我不直接跟你说定义, 因为通常而言, 定义所描述的概念很晦涩, 比如关于闭包的定义闭包是函数和声明该函数的词法环境的组合
所以, 我们先来看一下, JavaScript 里到底继承与原型链是如何表现的
继承与原型链像什么
不同于 Java 等的静态语言, 在 JavaScript 这门语言里, 我们没有类这个概念, 所有的继承都是基于原型的我们先直接看个例子:
- var obj = {
- a: 0,
- f: function() {
- return this.a + 1
- }
- }
- var obj_1 = {} // 我们期望 cat 也能有 sound 属性跟 speak 方法
- obj_1.__proto__ = obj console.log(obj_1.a) // 0
- console.log(obj_1.f()) // 1
如上, 我们定义 obj_1 这个对象的时候, 并没有声明 a 属性跟 f 方法, 但是我们依然可以找到它们这是因为在 JavaScript 中, 你在一个对象上寻找某个属性(JavaScript 对象都是键值对的形式, 所以方法其实也可以算一个属性), 他首先会在该对象本地寻找, 如果没有, 他会顺着原型链一层层往上寻找
在上面的栗子中, 对象 obj_1 本地没有定义任何属性, 所以当我们执行 obj_1.a 的时候, 会顺着原型链往上找在
obj_1.__proto__ = obj
这句里, 我们将 obj 赋值给了 obj_1 的__proto__属性
但是等等,__proto__是什么?
__proto__属性指向的就是 obj_1 的原型, obj 的原型是什么呢? 我们可以打印 obj.__proto__来看看, 结果打印出来一大堆东西, 这些其实就是 Object.prototype, 也就是终极原型, 这个对象不再继承任何原型按照之前说的, obj_1 应该也能直接访问到这上面的属性事实也的确如此, 比如:
obj_1.hasOwnProperty('a') // false
我们并没有在 obj_1 上定义 hasOwnProperty 方法, 但是依然可以找到该方法事实上, 所有以对象字面量 (Object Literal) 形式创建出来的对象, 都继承了有 Object.prototype 上的所有属性
那么我们能不能创建一个不继承自任何原型的对象呢? 答案是可以的
JavaScript 为我们提供了一个方法叫 Object.create, 通过它, 我们可以创建一个原型为特定对象的对象如果我们传入一个 null, 那么我们就能创建一个原型为空的对象
var a = Object.create(null)
在这个例子里, a 成了一个空的对象, 不仅本地没有任何属性, 连原型链都没有, 也就是说它甚至都没有继承 Object.prototype(思考: 这样的空对象到底有什么作用呢?)
这样一来, 我们也可以利用 Object.create 来实现继承咯? 对的
- var obj = {
- a: 0,
- f: function() {
- return this.a + 1
- }
- }
- var obj_2 = Object.create(obj) console.log(obj_2.a) // 0
- console.log(obj_2.f()) // 1
但是重新想象, 继承的本质是什么? 继承原型! 那么不管用什么方法, 只要在我的原型链上能找到你就行了
现在有一个问题, obj 上定义了一个属性 a, 如果我在 obj_2 上再定义一个属性 a, 那么打印出来的会是谁的 a 呢?
- var obj = {
- a: 0,
- f: function() {
- return this.a + 1
- }
- }
- var obj_2 = Object.create(obj)
- obj_2.a = 2
- console.log(obj_2.a) // 2
答案是显而易见的, 因为我们在寻找一个属性的时候, 总是从当前对象本地开始的, 如果在当前对象上找到了这个属性, 那么查询就停止了所以, 如果原型链过长, 在查找一个靠前的原型上的属性的时候, 就会比较耗时我们应当尽量避免这种过长的原型链
继承与原型链是什么
读到这里, 相信我们已经能够对继承和原型链做一个定义了
原型链
原型链就是从一个对象的__proto__开始, 一直到这条线的最末端, 大部分情况下, 这个最末端就是 Object.prototype 例如上面的那个例子:
- var obj = {
- a: 0,
- f: function() {
- return this.a + 1
- }
- }
- var obj_2 = Object.create(obj)
- // obj_2.__proto__ === obj
- // obj.__proto__ === Object.prototype
继承
在这个例子里,
obj---Object.prototype
就组成了一个原型链, 顺着原型链, 我们可以找到这个对象最开始继承自哪个对象, 同时, 原型链上的每一个节点都可以继承上游对象的所有属性继承描述的应该是一种关系, 或者一种动作
new 运算符
在前面的篇幅里我们知道, 在 JavaScript 里, 对象可以用字面量的形式与 Object.create 的形式来创建但是 JavaScript 里还有一种方式来创建一个对象, 那就是使用 new 运算符
var obj = new Object console.log(obj) // {}
根据前面的内容, 我们可知 obj 继承了 Object.prototype 对象上的属性关于 new 操作符, 可以看我的另一篇专栏当我们在 JavaScript 中 new 一个对象的时候, 我们到底在做什么那么 Object 是什么?
我们来执行一下 typeof Object, 打印出来的是 "function" 对的, Object 是一个函数, 准确地说, 它是一个构造函数 new 运算符操作的, 应该是一个函数
我们可以对任意函数执行 new 操作但是一个函数如果被用作了构造函数来实例化对象, 那我们倾向于把它的首字母大写
- var Foo = function(x) {
- this.x = x
- }
- var boo = new Foo(1) console.log(boo, boo.x) // Foo {x: 1} 1
构造函数能让我们初始化一个对象, 在构造函数里, 我们可以做一些初始化的操作通常我们在编写一些 JavaScript 插件的时候会在全局对象上挂载一个构造函数, 通过实例化这个构造函数, 我们可以继承它的原型对象上的所有属性
既然构造函数有属于自己的原型对象, 那么我们应该能让另一个构造函数来继承他的原型对象咯?
- var Human = function(name) {
- this.name = name
- }
- var Male = function(name) {
- Human.call(this, name)
- this.gender = 'male'
- }
- var jack = new Male('jack')
- console.log(jack) // Male {name: "jack", gender: "male"}
我们在构造函数内部执行了 Human 函数并将 this 指向了当前 Male 的原型对象上同时, 我们在 Male 的原型上定义一个自己的属性 gender, 这样, 实例化出来的对象同时有了两个属性
但是这个继承完整么? 继承是需要继承原型的, 但是 jack 的原型链上并没有 Human, 我们需要额外两步
- var Human = function(name) {
- this.name = name
- }
- var Male = function(name) {
- Human.call(this, name)
- this.gender = 'male'
- }
- Male.prototype = Object.create(Human.prototype)
- Male.prototype.constructor = Male
- var jack = new Male('jack')
- console.log(jack) // Male {name: "jack", gender: "male"}
这样一来, 我们就能在 jack 的原型链上找到 Human 了
ES6 的类
其实前面一节看起来会比较晦涩, 因为在 ES6 之前, JavaScript 没有类的概念(当然之后也没有), 但是我们却有构造函数, 那上面一节的栗子就应该说是构造函数 Male 继承了构造函数 Human?
我记得当时场面有点尴尬, 大家都搓着手低着头都不知道说点儿什么
好在 ES6 里我们有了 Class 的关键字, 这是个语法糖, 本质上, JavaScript 的继承还是基于原型的但是, 至少形式上, 我们可以按照类的方式来写代码了
- class Human {
- constructor(name) {
- this.name = name
- }
- }
- class Male extends Human {
- constructor(name) {
- super(name) this.gender = 'male'
- }
- }
- var jack = new Male('jack') console.log(jack) // Male {name: "jack", gender: "male"}
在控制台上顺着__proto__一层层往下翻, 我们会能找到 class Male 跟 class Human, 这说明我们的继承成功了同时, 我们也可以理解成类 Male 继承了类 Human, 虽然在 JavaScript 其实并没有类这个东西
结语
其实通篇的核心还是那句话: JavaScript 的继承是基于原型的很多内容我没有展开讲解很多, 表达了主干即可
引用
继承与原型
来源: https://segmentfault.com/a/1190000013214414