本文基于作者自身对前端知识的再次巩固和加深,适用于已经有相关知识基础的前端开发者,欢迎大家一起讨论前端学习中的感悟和收获。
原型和原型链
感觉每一次重温这个话题,都不是很贯通,总结了一遍又一遍,希望今天是最后一次。。
原型
什么是原型?原型就是一个对象,每个构造函数都有一个prototype属性,这个属性指向该构造函数的原型。原型上面提供了很多属性和方法给构造函数的所有实例进行共享。原型上面有一个constructor属性,该属性指向它的构造函数。
原型链
原型对象也是一个对象,那么这个对象上应该也存在一个__proto__属性,这个属性指向另一个构造函数的原型。所以我们只需要找到这个对象的构造函数是谁?在js里面,对象的构造函数就是Object(),对象的原型就是Object.prototype,由于在js中所有的数据类型都继承于Object,所以Object()没有再往上一层的原型对象,即Object.prototype=null。
在实际开发场景中,会出现这样的场景:当前原型是另一个构造函数的实例,即当前原型存在一个指针(proto)指向另一个的原型,相应地,另一个原型也有一个指针指向另一个构造函数,因此形成了原型链。
V8引擎
V8引擎的执行原理:
1. Ignition的作用?
- 将AST转换成字节码;
- 解析执行字节码,收集优化编译所需要的信息。
2. 优化编译的场景? 有下面这样一个求和函数:
function sum(a, b) {
return a + b;
}
sum(1, 2);
sum(2, 3);
sum(3, 4);
sum("hello", "world");
Ignition解释器发现sum函数重复执行了很多次,于是就会将它生成的字节码以及分析数据传给TurboFan编译器,TurboFan会根据分析数据生成高度优化的机器码,这样在每次重复调用sum函数的时候,就不用再重复生成字节码,直接使用编译之后的机器码就行。
但同时有这样一个问题:当执行到sum("hello", "world")时,sum函数传入的参数是两个字符串,函数执行的结果是对两个字符串的拼接,而不是相加,所以这里没有办法继续使用线程的机器码,因此,需要对机器码进行Deoptimization,将机器码转换成字节码,然后重新对字节码进行分析。这个问题也说明,调用函数的时候尽量保证传入的参数类型是一致的,否则性能会降低。
3. 为什么不直接将AST转换成机器码?
只被运行一次或者非热点的代码以字节码的形式可以更加紧凑的存储。由于字节码更小,编译的时间将会大幅减少,机器码会占用很大的内存。而且字节码是支持跨平台使用的。- 字节码可以传给
TurboFan进行优化编译,避免重复编译源代码。没有了这一步,就失去了一次中间优化的过程.
执行上下文和作用域链
js引擎内部,有一个执行上下文栈,用于执行代码的调用栈。
在js源码被翻译成可执行代码后,会创建执行上下文。每一个执行上下文都会关联一个VO(Variable Object 活动对象),变量和函数声明会被添加到这个VO对象中。
在执行一段script代码时,首先会创建全局执行上下文,将所有的变量和函数声明添加到全局对象中,这时所有的变量都还没有值,对于函数来说,会为它们创建一个函数对象,这个对象中包括:name、length、[[scopes]]属性等,在全局对象中,函数的值为它在内存中的地址。全局代码执行时,VO就是全局对象。
在正式执行全局代码时,会对变量进行赋值,如果遇到函数调用,首先会创建函数执行上下文,然后将它添加到执行上下文栈中。由于每个执行上下文都会关联一个VO,而对于函数执行上下文来说,它会创建一个AO(Activation Object),这个AO对象使用arguments对象进行初始化,并且初始值是传入的参数,这个AO会作为执行上下文的VO来存放变量的初始化。
如果一个函数被重复调用,会重复创建AO然后对变量进行赋值,每次函数执行完,在一般情况下,这个AO会被垃圾回收机制回收掉,这个函数执行上下文也会从栈顶被弹出。
当进入到一个执行上下文时,还会关联到一个作用域链。作用域链是一个列表,用于变量标识符的求值。创建作用域链时,会向其中添加一系列对象。函数在定义时,作用域链就已经确定好了,和调用的位置没有关系。
如果一个变量未经声明就直接赋值,不管它是在全局作用域,还是在函数作用域中,它都会被添加到全局对象中。
总结:
变量提升、函数提升是在变量对象被创建的过程中进行的。当遇到变量和函数都会被提升的情况,函数声明的优先级高于变量声明,因此出现同名属性的提升时,后面还可以重新赋值。