图解 Google V8 # 05:函数表达式的底层工作机制

本文涉及的产品
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 图解 Google V8 # 05:函数表达式的底层工作机制

说明

图解 Google V8 学习笔记



函数声明与函数表达式的差异


我们先来看两段代码的对比:

代码片段1:

foo()
function foo(){
    console.log('foo')
}


执行代码,发现没有报错,并且成功打印了 foo。


b93cc96e07b94d389c99bc150d5f5867.png


代码片段2:

foo()
var foo = function (){
    console.log('foo')
}


同样我们执行代码,发现代码执行报错了:Uncaught TypeError: foo is not a function

8ac84655961043f0939addb207dbfff5.png


都是在定义的函数之前调用函数,为什么代码片段1执行成功,而代码片段2执行报错?


原因:这两种定义函数的方式具有不同语义,不同的语义触发了不同的行为。


我们把第一种称之为函数声明

896fd0f22e384fb6993b17bffda1312c.png

第二种称之为函数表达式

274b1b076f1f41a399d81b5e3531eb71.png


函数声明与函数表达式区别


函数表达式是在表达式语句中使用 function 的,最典型的表达式是 a=b 这种形式,因为函数也是一个对象,我们把 a = function (){} 这种方式称为函数表达式;


在函数表达式中,可以省略函数名称,从而创建匿名函数(anonymous functions)


一个函数表达式可以被用作一个立即调用的函数表达式(IIFE)(Immediately Invoked Function Expression)


V8 是怎么处理函数声明的?


先看一个例子:

var x = 5
function foo(){
    console.log('Foo')
}


V8 执行这段代码的流程大致如下:


在编译阶段:

   如果解析到函数声明,那么 V8 会将这个函数声明转换为内存中的函数对象,并将其放到作用域中。

   如果解析到了某个变量声明,也会将其放到作用域中,但是会将其值设置为 undefined,表示该变量还未被使用。


在执行阶段:

   如果使用了某个变量,或者调用了某个函数,那么 V8 便会去作用域查找相关内容。


182383528a764c68a78c4b2ac4971488.png


查看作用域的数据

没有d8环境的可以参考我这篇文章window 系统里怎么使用 jsvu 工具快速调试 v8?

9f58d9b83c8e488ba4926d16161a5966.png


我们在 kaimo.js 里添加上面的代码


8a9d806f1794402aa4b50bb6025102ee.png


然后执行命令

v8-debug --print-scopes kaimo.js


执行之后我们就能看到作用域的数据

8cab7ca5c08744d5864d45459a3cb3dc.png


Inner function scope:
function foo () { // (00000254208756A0) (23, 53)
  // NormalFunction
  // 2 heap slots
}
Global scope:
global { // (00000254208753F0) (0, 53)
  // will be compiled
  // NormalFunction
  // 1 stack slots
  // temporary vars:
  TEMPORARY .result;  // (0000025420875900) local[0]
  // local vars:
  VAR foo;  // (0000025420875870)
  VAR x;  // (0000025420875610)
  function foo () { // (00000254208756A0) (23, 53)
    // lazily parsed
    // NormalFunction
    // 2 heap slots
  }
}



我们可以看到,作用域中包含了变量 x 和 foo,变量 x 的默认值是 undefined,变量 foo 指向了 foo 函数对象,foo 函数对象被 V8 存放在内存中的堆空间了,这些变量都是在编译阶段被装进作用域中的。

在编译阶段,将所有的变量提升到作用域的过程称为变量提升。


表达式和语句

在 V8 解析 JavaScript 源码的过程中,如果遇到普通的变量声明,那么便会将其提升到作用域中,并给该变量赋值为 undefined,如果遇到的是函数声明,那么 V8 会在内存中为声明生成函数对象,并将该对象提升到作用域中。


这是为什么呢?这跟表达式和语句有关。


简单的说:表达式就是表示值的式子,而语句是操作值的式子。


举个例子:

x = 1
1 === 2


上面这些就是表达式,因为执行这段代码,它会返回一个值。

而定义了一个变量,比如:

var x



就是一个语句,执行该语句时,V8 并不会返回任何值给你。

在比如:声明了一个函数时

function foo(){
  return 1
}


执行这段代码时,V8 并没有返回任何的值,它只是解析 foo 函数,并将函数对象存储到内存中。

我们在看一个声明变量赋值的例子:

var x = 313



V8 执行这段代码时,会认为它是两段代码:

  • var x = undefined:定义变量的语句,在编译阶段完成。V8 将这些变量存放在作用域时,还会给它们赋一个默认的 undefined 值。
  • x = 313:赋值的表达式,在执行阶段完成的,不会在编译阶段执行的。



函数声明是表达式还是语句呢?


函数声明并不是一个表达式,而是一个语句。另外函数也是一个对象,在编译阶段,V8 就会将整个函数对象提升到作用域中,并不是给该函数名称赋一个 undefined


V8 是怎么处理函数表达式的?


在一个表达式中使用 function 来定义一个函数,那么就把该函数称为函数表达式

foo = function (){
    console.log('foo')
}


函数表达式

例子:

var foo = function (){
    console.log('foo')
}


可以把这段代码拆分为下面两行代码:

var foo = undefined


这一行是声明语句, V8 在解析阶段,会在作用域中创建该对象,并将该对象设置为 undefined,

foo = function (){
    console.log('foo')
}


这一行是函数表达式,在编译阶段,V8 并不会处理函数表达式,也就不会将该函数表达式提升到作用域中。



立即调用的函数表达式(IIFE)

(function () {
})


dcc420abc7254895b0580132dd0d75a6.png


因为小括号之间存放的必须是表达式,如果在小阔号里面定义一个函数,那么 V8 就会把这个函数看成是函数表达式,执行时它会返回一个函数对象。如果直接在表达式后面加上调用的括号,这就称为立即调用函数表达式(IIFE)


比如下面这个就是 IIFE

(function () {
})()


函数立即表达式也是一个表达式,所以 V8 在编译阶段,并不会为该表达式创建函数对象。这样的一个好处就是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到。


因为函数立即表达式是立即执行的,所以将一个函数立即表达式赋给一个变量时,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。

比如:

var kaimo = (function () {
    return 777
})()


4b251093709c442ab2b62aee0b6690a7.png



经典面试题

通过上面的学习,这两个题目应该得心应手。【手动狗头】

var n = 1;
(function foo(){
    n = 100;
    console.log(n);
}())
console.log(n);


475e027828e34e9ebd0789f3875fa239.png

var n = 1;
function foo(){
    n = 100;
    console.log(n);
}
console.log(n);
foo()


b0489330e1074ea48f01d06d131927d9.png

目录
相关文章
|
Web App开发 缓存 JavaScript
图解 Google V8 # 13:字节码(一):V8为什么又重新引入字节码?
图解 Google V8 # 13:字节码(一):V8为什么又重新引入字节码?
236 0
图解 Google V8 # 13:字节码(一):V8为什么又重新引入字节码?
|
缓存 JavaScript 前端开发
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
307 0
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
|
Web App开发 JavaScript 前端开发
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
118 0
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
|
算法 JavaScript Java
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
102 0
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
|
前端开发 JavaScript
图解 Google V8 # 19 :异步编程(二):V8 是如何实现 async/await 的?
图解 Google V8 # 19 :异步编程(二):V8 是如何实现 async/await 的?
132 0
图解 Google V8 # 19 :异步编程(二):V8 是如何实现 async/await 的?
|
消息中间件 前端开发 JavaScript
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
422 0
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
|
消息中间件 程序员 Android开发
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
109 0
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
|
存储 缓存 索引
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
145 0
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
|
JavaScript 前端开发 编译器
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
140 0
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
|
JavaScript 前端开发 Java
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?
242 0
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?