- Published on
V8相关(三)
- Authors
- Name
- Et cetera
- 原型链:V8 如何实现对象继承
- JavaScript 如何先基于原型链继承
- 构造函数是怎么创建对象的?
- 构造函数怎么实现继承?
- 小历史
- V8 是如何查找变量的:作用域链
- 什么是函数作用域和全局作用域?
- 作用域链怎么工作的
原型链:V8 如何实现对象继承
首先明确,什么是继承? 继承
:就是一个对象可以访问另外一个对象中的属性和方法
而不同语言实现继承的方式不同,最经典的两种方式:基于类的设计
(如C++
、Java
、C#
)和基于原型继承的设计
(如JavaScript
)
使用基于类的继承
时,如果业务复杂,那么你需要创建大量的对象,然后需要维护非常复杂的继承关系,这会导致代码过度复杂和臃肿,另外引入了这么多关键字也给设计带来了更大的复杂度
JavaScript 如何先基于原型链继承
JavaScript 的每个对象
都包含了一个隐藏属性 __proto__
,我们就把该隐藏属性 __proto__
称之为该对象的原型 (prototype)
,__proto__
指向了内存中的另外一个对象,我们就把 __proto__
指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性

上图可以看到,对象 A
有个属性是 color,那么通过 C.color
访问 color 属性时,V8 会先在 C 对象
内部查找,但是没有查找到,接着继续在 C 对象的原型对象 B
中查找,但是依然没有查找到,那么继续去对象 B 的原型对象 A
中查找,因为 color 在对象 A
中,那么 V8 就返回该属性值
我们看到使用 C.name
和 C.color
时,给人的感觉属性 name 和 color 都是对象 C
本身的属性,但是实际上这些属性都是位于原型对象上,我们把这个查找属性的路径称为原型链
,它像一个链条一样,将几个原型链接了起来
在这里还要注意一点,不要将原型链接
和作用域链
搞混淆了,作用域链
是沿着函数的作用域
一级一级来查找变量的,而原型链
是沿着对象的原型
一级一级来查找属性的,虽然它们的实现方式是类似的,但是它们的用途是不同的
小结:继承就是一个对象可以访问另外一个对象中的属性和方法,在 JavaScript 中,我们通过原型和原型链的方式来实现了继承特性
构造函数是怎么创建对象的?
function DogFactory(type, color) {
this.type = type
this.color = color
}
var dog = new DogFactory('Dog', 'Pink')
通过这种方式,我们就把后面的函数称为构造函数
,因为通过执行 new
配合一个函数,JavaScript 虚拟机便会返回一个对象
其实当 V8 执行上面这段代码时,V8 会在背后悄悄地做了以下几件事情,模拟代码如下所示:
// new 关键字底层基本原理
var dog = {}
dog.__proto__ = DogFactory.prototype
DogFactory.call(dog, 'Dog', 'Pink')

构造函数怎么实现继承?
每个函数对象中都有一个公开的 prototype 属性
,当你将这个函数作为构造函数来创建一个新的对象时,新创建对象的原型对象就指向了该函数的 prototype 属性
.当然了,如果你只是正常调用该函数,那么 prototype 属性
将不起作用
现在我们知道了新对象的原型对象指向了构造函数的 prototype 属性
,当你通过一个构造函数创建多个对象的时候,这几个对象的原型都指向了该函数的 prototype 属性
,如下图所示:

小历史
new
关键字在最早的JavaScript
中并不存在,但是为了秉持蹭Java
热度到底,于是加入了在 Java 中创建对象频繁使用的 new
关键字
最后,在 JavaScript
中,并不建议直接使用 __proto__
属性,主要有两个原因:
- 这是隐藏属性,并不是标准定义的
- 使用该属性会造成严重的性能问题
V8 是如何查找变量的:作用域链
作用域就是存放变量和函数的地方,全局环境有全局作用域,全局作用域中存放了全局变量和全局函数.每个函数也有自己的作用域,函数作用域中存放了函数中定义的变量
什么是函数作用域和全局作用域?
每个函数在执行时都需要查找自己的作用域,我们称为函数作用域,在执行阶段,在执行一个函数时,当该函数需要使用某个变量或者调用了某个函数时,便会优先在该函数作用域中查找相关内容
全局作用域和函数作用域类似,也是存放变量和函数的地方,但是它们还是有点不一样:
全局作用域中包含了很多全局变量,比如全局的 this 值,如果是浏览器,全局作用域中还有 window
、document
、opener
等非常多的方法和对象,如果是 node 环境
,那么会有Global
、File
等内容
V8 启动之后就进入正常的消息循环状态
,这时候就可以执行代码了,比如执行到上面那段脚本时,V8 会先解析顶层 (Top Level) 代码
,我们可以看到,在顶层代码中定义了变量 x,这时候 V8 就会将变量 x 添加到全局作用域中
作用域链怎么工作的
var name = '爱缪'
var type = 'band'
function foo() {
var name = 'foo'
console.log(name)
console.log(type)
}
function bar() {
var name = 'bar'
var type = 'function'
foo()
}
bar()
// -----解析阶段-变量提升-----
var name = undefined
var type = undefined
function foo() {
var name = 'foo'
console.log(name)
console.log(type)
}
function bar() {
var name = 'bar'
var type = 'function'
foo()
}
// -----执行阶段-----
name = '爱缪'
type = 'band'
bar()
因为 JavaScript 是基于词法作用域
的,词法作用域就是指,查找作用域的顺序是按照函数定义时的位置
来决定的。bar 和 foo 函数的外部代码都是全局代码,所以无论你是在 bar 函数中查找变量,还是在 foo 函数中查找变量,其查找顺序都是按照当前函数作用域–> 全局作用域这个路径来的
另外,因为词法作用域是根据函数在代码中的位置来确定的,作用域是在声明函数时就确定好的了,所以我们也将词法作用域称为静态作用域
和静态作用域
相对的是动态作用域
,动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用
换句话说,作用域链
是基于调用栈
的,而不是基于函数定义的位置
的