JavaScript重难点解析3(原型与原型链、执行上下文与执行上下文栈)
原型与原型链
原型(prototype)
每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
原型对象中有一个属性constructor, 它指向函数对象
function A() {
}
console.log(A.prototype) // 打印一个空对象
console.log(A.prototype.constructor===A) //true
给原型添加的方法可以直接调用
function Aaa() {
}
Aaa.prototype.test = function () {
console.log(' Aaa')
}
var aaa = new Aaa()
aaa.test() //打印Aaa
显示原型与隐式原型
每个构造函数都有一个prototype,即显式原型(属性)
每个实例对象都有一个__proto__,可称为隐式原型(属性)
对象的隐式原型的值为其对应构造函数的显式原型的值
function Fn() { // 内部语句: this.prototype = {}
}
//创建实例对象
var fn = new Fn() // 内部语句: this.__proto__ = Fn.prototype
console.log(Fn.prototype===fn.__proto__) // true
Fn.prototype.test = function () {
console.log('test()')
}
fn.test()
内存结构图
原型链
在访问一个对象的属性时,首先在自身属性中查找,如果没有找到, 再沿着__proto__这条链向上查找, 如果还是没有, 则返回undefined。
function Fn() {
}
console.log(Fn.prototype)
Fn.prototype.test = function () {
console.log('test')
}
var fn = new Fn()
fn.test() //调用自己的test1
fn.toString() //调用Object上的toString方法
这里对该结构做一个简单介绍,在所有代码执行之前JS引擎会自动创建Object原型对象,并自动生成Object构造方法(相当于定义Function Object() {…}),Object构造方法的prototype属性指向Object原型对象,还会生成函数构造方法(相当于定义Function Function() {…})和一个函数原型对象(也是一个Object对象)Function()的prototype和__proto__都指向函数原型对象,当我们代码执行到 function Fn() 时,堆中会创建一个Function的实例对象,Function对象的prototype指向函数原型对象。当执行到 Fn.prototype.test= function () {…}时,在空object对象上创建了test方法,当调用fn.test时,fn会在自身属性中查找,发现没有然后沿着__proto__查找,找到后执行。
最后做几点特殊说明:
- 函数的显示原型指向的对象默认是空Object实例对象(但Object不满足)
- 所有函数都是Function的实例(包含Function)
- Object的原型对象是原型链尽头
instanceof是如何判断
表达式: A instanceof B
如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false
简单来说,看上例中画的图,如果A沿着__proto__走可以走到B的prototype上,那么就是true,不然就是false。
大家可以按照上例中的图对这几个例子画一画:
function Foo() { }
var f1 = new Foo()
console.log(f1 instanceof Foo) // true
console.log(f1 instanceof Object) // true
console.log(Object instanceof Function) // true
console.log(Object instanceof Object) // true
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true
console.log(Object instanceof Foo) // false
原型链是JS的一大难点,不好理解,如果大家看完感觉不解的化可以根据自己的代码画图看看。多画几遍比多听要强很多。
执行上下文与执行上下文栈
变量提升与函数提升
变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
var a = 3
function fn () {
console.log(a) //undefined
var a = 4
}
fn()
根据变量提升和函数提升的概念以上程序可以翻译为:
var a
function fn () {
var a
console.log(a)
a = 4
}
var a = 3
fn()
所以a为undifined。
在这里可以说明一个两种函数定义方法的区别:
fn2() //可调用 函数提升
fn3() //不能 变量提升
function fn2() {
console.log('fn2()')
}
var fn3 = function () {
console.log('fn3()')
}
执行上下文
全局执行上下文
在执行全局代码前将window确定为全局执行上下文
对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
函数执行上下文
在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
执行上下文栈
在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
在函数执行上下文创建后, 将其添加到栈中(压栈)
在当前函数执行完后,将栈顶的对象移除(出栈)
当所有的代码执行完后, 栈中只剩下window
以一道面试题为例:
console.log('gb: '+ i)
var i = 1
foo(1)
function foo(i) {
if (i == 4) {
return
}
console.log('fb:' + i)
foo(i + 1) //递归调用: 在函数内部调用自己
console.log('fe:' + i)
}
console.log('ge: ' + i)
执行上下文栈如图:
- window入栈,由于变量提升,程序开始i被定义为undifined,所以第一个打印console.log打印gb:undifined。打印完下一行对i赋值1。
- 程序执行到第三行foo(1)函数入栈,形参i=1不为4,直接执行console.log(‘fb:’ + i)打印fb:1。
- foo(i + 1)处一个新的foo(2)函数入栈,形参i=2,重复2的操作打印fb:2。
- foo(i + 1)处一个新的foo(3)函数入栈,形参i=3,重复2的操作打印fb:3。
- foo(i + 1)处一个新的foo(4)函数入栈,形参i=4返回,foo(4)出栈。
- foo(3)继续执行,到console.log(‘fe:’ + i)打印fe:3,foo(3)执行结束出栈。
- foo(2)继续执行,到console.log(‘fe:’ + i)打印fe:2,foo(2)执行结束出栈。
- foo(1)继续执行,到console.log(‘fe:’ + i)打印fe:1,foo(1)执行结束出栈。
- window继续执行,此时i为1,console.log('ge: ’ + i)处打印ge:1。