在JavaScript中,函数其实也是一个对象,所以函数也可以有自己的属性和值,它与对象不同的地方在于,它可以被执行,执行时会创建函数作用域。
我们知道,在执行函数的时候,我们只需要在函数名后面,加上一个括号,在括号里传入参数,就可以执行这个函数了,但是,V8是怎么做到执行这个函数的,V8如何找到这个函数的相关代码
既然函数是一个对象,那么它就有自己的属性,除了我们给它添加的属性,函数本身有两个属性,分别是name和code。
name属性存储着函数的名字,而code以字符串的形式存储函数
就像下面的代码
function fn(){
// ...
}
console.log(fn.name)
// fn
当我们执行代码的时候,V8会去找到这个函数对应的code属性,然后解释执行code属性的值,也就是原函数。
我们要知道,V8是使用栈结构来管理函数调用的,所以我们经常可以听到函数调用栈,但为什么使用栈结构来管理函数调用呢?
除了栈结构本身的特点:空间连续和先进后出外,也与函数本身有关。
首先,函数是可以被调用的,我们可以在一个函数里面去调用另外一个函数,当我们调用一个函数的时候,代码执行的控制权将由父函数或者全局作用域转移到子函数,而当函数调用完毕后,会返回给父函数或者全局,这与栈结构的先进后出刚好吻合。
其次,函数具有作用域机制,函数内部的变量,被我们称为局部变量,只要我们不暴露出去,就只能在函数内部被访问到,即使返回,也只是返回一个值或者一个对象的地址,所以一般情况下,当我们一个函数调用完毕时,函数内部的局部变量都该被销毁,而通过使用栈结构,当函数执行完毕出栈时,空间里对应的变量也被清除。
什么是一等公民,对一种语言来说,如果它的函数和它的基本类型能完成一样的事情,比如可以赋值给变量,可以做为函数的参数,可以做为函数的返回值,可以做为对象的属性值等,那么就说这种语言的函数是一等公民。
函数成为一等公民,让我们可以方便地去传递函数,但也正是因为函数是一等公民,带来了一些麻烦。当我们在函数内部去引用外部变量的时候,V8需要去判断,这个外部变量是否真的存在,闭包就是一个典型的例子。如下面的代码
function fn(){
let count = 0
function add(){
count++
console.log(count)
}
return add
}
let a = fn()
a()
这里fn内部声明了一个函数add,而这个add函数,调用了它所在的函数作用域里的count变量,所以即使fn已经调用完了,因为add函数内部用到了fn函数作用域里的变量,所以V8不得不去维护这个作用域,不能去销毁这个作用域,这就是函数做为一等公民带来的麻烦。
首先,我们说说闭包,为什么JavaScript会有闭包,有以下三点
再有一个,我们要明白,JavaScript出于加快初次解析和减少内存浪费的目的,采用了惰性解析:当解析代码的时候,对函数声明只转换成对用的对象,而不解析函数内部的内容,
我们可以看看下面这个闭包
function fn(){
var fnNum = 1
return function inner(){
var innerNum = 2
return fnNum+innerNum
}
}
var f = fn()
在这里,inner就是一个典型的闭包,这个闭包带来了一定的问题。当我们调用fn函数时,它会将里面的函数inner返回给变量f,然后当函数执行完毕的时候,fn的执行上下文会被V8销毁,但是,我们可以看到,闭包inner调用了fn内的变量fnNum,这导致了fnNum不能被销毁,但是这样就带来问题了。
我们知道,这段代码是可以实现的,而且执行f()
的时候,会返回3,那就意味着fnNum没用被销毁,那么我们怎么做到它不被销毁,同时,上面提到了,V8采用了惰性解析,既然采用了惰性解析,那也就意味着,当我们执行fn()
之前,我们是不会去解析inner函数内部的代码的,那我们又怎么知道,fnNum被inner函数使用了呢。
要解决这个问题,我们就需要去判断函数内部是否会用到父函数的变量,V8引入了预解析器去执行这个任务。
引入预解析器后,V8遇到一个函数时,不会直接跳过该函数,而是会对该函数进行以此快速的预解析,预解析有两个工作
函数表达式是JavaScript中非常重要的基础内容,我们可以使用函数表达式来执行一个函数,同时又不会把实现的逻辑代码暴露出来。
函数表达式说简单,看起来不难,但实际上,还是涉及到挺多东西的。它涉及到一些底层概念,什么是语句,什么是表达式,函数即对象的概念。我们要清楚这些概念,才能更好地去理解函数表达式。
我们知道,在JavaScript里面,一般情况下,我们可以有两种方式去声明一个函数
// 函数声明
function fn(){
// ...
}
// 函数表达式
var fn = function(){
// ...
}
上面的两行代码,虽然最终都会得到一个函数fn,但是还是存在不同的。
哪里不同,其实从名字就能看出来了,函数声明和函数表达式,既然有声明,那就可能会涉及到JavaScript非人的设计,变量提升。V8在执行JavaScript代码的时候,会先创建全局作用域,然后对代码进行编译,而在编译期间,会把声明内容放到全局作用域中,像下面的这段代码
fn()
function fn(){
console.log('fn')
}
V8处理时会变为
// 编译阶段
function fn(){
console.log('fn')
}
// 执行阶段
fn()
而对于函数表达式,如下代码
fn()
var fn = function(){
console.log('fn')
}
V8处理时会是
// 编译阶段
var fn = undefined
// 执行阶段
fn()
fn = function(){
console.log('fn')
}
所以这段代码会报错,因为在fn执行的时候,fn还不是方法。
实际上,这就涉及到了表达式和语句的区别了,表达式在编译阶段是不会执行的。
我们可以简单地区分,表达式,是JavaScript中的一个短语,JavaScript编译执行它会返回一个结果,而语句,是一个完整的句子,它可以单独执行。就像下面的函数表达式
(function(){
//...
})()
我们如果不加括号,那么function(){}
是会报错的,因为编译器看到开头是function,将其看做一个函数声明语句,而函数声明语句function后面要加函数名,但是,放到括号里后,它就被看成一个表达式了,这个括号内通过JavaScript编译器“计算”得到一个函数,然后使用()调用。
我们能看到,函数表达式和函数声明的在创建一个函数时的区别,函数声明可以让声明的函数在编译阶段就放到作用域中,也即是说可以在声明前就调用这个方法了,而函数表达式,可以做为一个匿名函数使用,可以不污染环境。
像上面举例用的的函数表达式
(function(){
//...
})()
这是一个立即执行的函数表达式(IIFE),使用这样的函数调用可以让我们不用去污染环境变量。
回调函数,说到底只是一个函数而已,我们可以在很多个场景见到回调函数,一个简单的定时器setTimeout或者setInterval,一个ajax异步回调,又或者一个forEach方法的回调函数参数,甚至于回调函数的滥用,让我们听到了回调地狱。
具体来说,就我们上面举到的例子其实可以看到了,回调函数分为同步回调和异步回调,同步回调是直接在执行函数内部执行的,而异步回调函数,往往会在另一个任务里面执行,像定时器,或者是ajax异步回调,它们的回调函数和本身所在的函数执行,是在两个宏任务的
对于同步回调函数,我们很容易理解,它在函数内部执行完后就执行,但是异步回调可能就不是很好理解了,这涉及到V8的事件循环机制和消息队列等。而这些又与V8的线程模型相关,所以我们要先分析一下V8的线程模型
首先我们要明白的是,JavaScript在浏览器中,是在UI线程里面执行的,这与早期浏览器只有UI线程有关。
所谓UI线程,是指运行窗口的线程,当你运行一个窗口时,无论该页面是Windows上的窗口系统,还是Android或者iOS上的窗口系统,它们都需要处理各种事件,诸如有触发绘制页面的事件,有鼠标点击、拖拽、放大缩小的事件,有资源下载、文件读写的事件,等等。
我们知道,上面说到的事件,往往会在同一段时间内发生,可能上一个事件没有处理完,下一个事件就来了,我们要如何控制这些事件的执行,才能保证CPU正常运行和事件有序执行。
针对这种情况,浏览器实现了一个消息队列,每当我们产生一个事件的时候,浏览器就将这个事件推入到队列中,然后依次把事件从队列中推出,因为队列的特点是先进先出,所以事件的执行,是按照事件触发的顺序,我们把UI线程每次从消息队列中取出事件,执行事件的过程称为一个任务。
对于定时器来说,会有另一个队列来存放定时器里面的回调函数,当我们执行一个定时器时,会将这个回调函数推入这个队列,然后又一个任务调度器来控制,当时间到了的时候,任务调度器就会将这个回调函数从队列中取出,压入到上面说的消息队列里面。
而对于像XHR这种和网络相关的异步回调,又存在了不同的处理。因为我们的XHR请求会去请求服务端的资源,而这个资源请求花费的时间,相对于我们一个普通的事件来说是很长的,如果只是获取一些数据还好,如果是下载一整个文件,那时间就不是以ms为单位了,甚至会以h为单位了。
而就像上面说的,JavaScript运行在UI线程上,如果我们在这里还是由UI线程来处理这个请求事件的话,那无疑会对页面造成严重的阻塞,为了解决这个问题,浏览器借助了网络线程的力量,由网络线程来处理数据的获取
当我们在UI线程,也就是JavaScript发起一个XHR任务时