预编译阶段:声明语句,如果是函数声明会直接赋值;预编译阶段结束会逐行执行,此时赋值语句才会执行
涉及:单线程、语法分析、预编译(GO、AO)、执行上下文、栈、作用域、解释执行、变量提升、函数提升、作用域链
- js是单线程语言:在浏览器中一个页面永远只有一个线程在执行js脚本代码,不会发生解析阻塞
js运行三部曲:语法分析、预编译、解释执行
-
语法分析:引擎检查你的代码有没有什么低级的语法错误
-
预编译:在内存中开辟一些空间,存放一些变量与函数——会导致变量提升
-
解释执行:执行代码
-
浏览器先按照js的顺序加载script标签分隔的代码块,js代码块加载完毕之后,立刻进入到上面的三个阶段,然后再按照顺序找下一个代码块,再继续执行三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的,并且都在同一个全局作用域中。
过程:
- 页面产生便创建了GO全局对象(Global Object)(也就是window对象);
- 第一个脚本文件加载;
- 脚本加载完毕后,分析语法是否合法;
- 开始预编译查找变量声明,作为GO属性,值赋予undefined; 查找函数声明,作为GO属性,值赋予函数体;
语法分析
- js的代码块加载完毕之后,会首先进入到语法分析阶段,该阶段的主要作用:分析该js脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误(syntaxError) ,停止改js代码的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入到预编译阶段。
预编译
-
GO(global object)对象(window对象)(脚本代码块script执行前)
-
生成GO对象 GO{}(global object) 这个GO就是window
-
将全局的变量声明(的名)储存一GO对象中,value为undefinde
-
将全局的函数声明的函数名作为go对象中的key,函数整体内容为value储存到GO对象中
-
任何变量,如果变量未经声明就赋值,这些变量就为全局对象所有。一切声明的全局变量和未经声明的变量,全归window所有。
var a = 123; window.a = 123; function test(){ // 这里的b是未经声明的变量,所以是归window所有的。 var a = b = 110; } -
例子
var a function fun() {} function abc() {} console.log('第1次', a) // function a function a() {} console.log('第2次', a) // function a var a = 100 console.log('第3次', a) // 100-
会生成一个对象(GO),这个对象封装的就是作用域,称为GO(global object)。当全部挂载完成之后,然后代码再去逐行执行
-
分析变量声明(var)——变量作为GO对象的属性名,值为undefined
-
分析函数声明(function)——函数名作为GO对象的属性名,值为函数体(如果遇到同名,直接覆盖)
GO={ a:undefined, fun:function fun(){}, abc:function abc(){} } -
当走到某一行的时候;a产生了一次赋值;此时GO对象改变了
-
逐行执行(看着GO对象里面的执行)
-
-
-
AO(action object)对象
-
是函数执行前的一瞬间,生成一个AO对象(在函数执行前的一瞬间会生成自己的AO,如果函数执行2次,生成了两次AO,这两次的AO是没有任何关联)
- 执行前的一瞬间,会生成一个AO(action object)对象
- 分析参数,形参作为AO对象的属性名,实参作为AO对象的属性值
- 分析var变量声明,变量名作为AO对象的属性名,值为undefined,如果遇到同名的,不去做任何改变
- 分析函数声明,函数名作为AO对象的属性名,值为函数体,如果遇到同名的,直接覆盖(会将作为参数传进来的变量值覆盖)
-
例子
var num = 100 function fun(num) { console.log(num) // 5 } fun(5) // 1.调用前的一瞬间,生成fun.AO对象 AO = {} // 2.分析参数 AO = { num : 5 } // 3.分析变量声明 没有略过 // 4.分析函数声明 没有略过 AO = { num : 5 }console.log(person) // undefined console.log(personFun) // function personFun var person = "saucxs"; console.log(person) // saucxs function personFun() { console.log(person) var person = "songEagle"; console.log(person) } personFun() // undefined songEagle console.log(person) // saucxs
-
变量提升、函数提升
-
函数声明整体提升-(具体点说,无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面)
-
变量声明提升-(具体点说,无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined)
function dd() { return 4 } var dd console.log(dd) //function dd()function test () { console.log(a); //undefined 变量提升 var a = 123 } function test () { console.log(a); // 报错 }a = 1; var a; console.log(a); //1
函数调用栈
-
每进入到一个不同的运行环境都会创建 一个相应的执行上下文(execution context),那么在一段js程序中一般都会创建多个执行上下文,js引擎会以栈的数据结构对这些执行进行处理,形成函数调用栈(call stack),栈底永远是全局执行上下文(global execution context),栈顶则永远时当前的执行上下文。
function bar() { var B_context = "bar saucxs"; function foo() { var f_context = "foo saucxs"; } foo() } bar()- 首先进入到全局环境,创建全局执行上下文(global Execution Context ),推入到stack中;
- 调用bar函数,进入bar函数运行环境,创建bar函数执行上下文(bar Execution Context),推入stack栈中;
- 在bar函数内部调用foo函数,则再进入到foo函数运行环境中,创建foo函数执行上下文(foo Execution Context),如上图,由于foo函数内部没有再调用其他函数,那么则开始出栈;
- foo函数执行完毕之后,栈顶foo函数执行上下文(foo Execution Context)首先出栈;
- bar函数执行完毕,bar函数执行上下文(bar Execution Context)出栈;
- 全局上下文(global Execution Cntext)在浏览器或者该标签关闭的时候出栈。
说明:不同的运行环境执行都会进入到代码预编译和执行两个阶段,语法分析则在代码块加载完毕时统一检查语法。
创建执行上下文
-
创建变量对象(variable object)(创建GO、AO对象)
-
创建作用域链(scope chain)
-
作用域链由当前执行环境的变量对象(未进入到执行阶段前)与上层环境的一系列活动对象组成,保证了当前执行还款对符合访问权限的变量和函数有序访问。
- 作用域链的第一项永远是当前作用域(当前上下文的变量对象或者活动对象);
- 最后一项永远是全局作用域(全局上下文的活动对象);
- 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或者函数,找到则会停止找,找不到则一直查找全局作用域,再找不到就会排除错误。
var num = 30; function test() { var a = 10; function innerTest() { var b = 20; return a + b } innerTest() } test() innerTestEC = { //变量对象 VO: {b: undefined}, //作用域链 scopeChain: [VO(innerTest), AO(test), AO(global)], //this指向 this: window } -
-
确定this的指向
作用域
-
全局作用域(GO)
- 全局作用域在页面打开时被创建,页面关闭时被销毁
- 编写在script标签中的变量和函数,作用域为全局,在页面的任意位置都可以访问到
- 在全局作用域中又全局对象window,代表一个浏览器窗口,由浏览器创建,可以直接调用
- 全局作用域中声明的变量和函数会作为window对象的属性和方法保存
-
函数作用域(AO)
- 调用函数时,函数作用域被创建,函数执行完毕,函数作用域被销毁
- 每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的
- 在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量
- 在函数作用域中访问变量、函数时,会先在自身作用域中寻找,若没有找到,则会在函数的上一级作用域中寻找,一直到全局作用域
作用域链
-
会被保存在一个隐式的属性中去[scope]这个属性是我们用户访问不到的,但是的的确确是存在的,让js引擎来访问的,里面存储的就是作用域链
-
作用域是AO和GO,作用域链是AO和GO的集合 例子
``` function a() { function b() { var bb = 123 a = 0 } var a = 234 b() } a() ``` -
a定义的时候作用域链首端指向全局GO
-
a执行和b定义同时进行,产生a的作用域AO,作用域链首端指向AO,作用域链的第二端指向全局作用域GO
-
b执行,顺序分别指向b的AO(this指向window),a的AO,全局的GO
闭包
-
例子
function a() { var a = 123 function b() { var bb =234 console.log(aa) // 123 } } var res = a() // a执行完毕要将作用域销毁 res() -
a执行完毕要将作用域销毁,但是b的作用域链没有销毁
-
b函数被保存在外部,但仍然可以指向a的作用域AO,这是产生闭包的原因
-
单例模式实现闭包
var createLogin = function() { var div = document.createElement('div') div.innerHTML = '我是登录的弹窗' div.style.display = 'none' document.body.appendChild(div) return div } var getSingle = function(fn) { var result return function() { return result || (result = fn.apply(this, arguments)) // 第二次result就有值了 } } var create = getSingle(createLogin) document.getElementById('loginBtn').onclick = function() { var loginLay = create() loginLay.style.display = 'block' }