js 随笔

作者: Lyan_2ab3 | 来源:发表于2020-03-05 12:52 被阅读0次

js 执行机制:

js执行上下文:

只有理解了js 执行上下文才能更好的理解 js变量提升以及 作用域和闭包


showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。

/*
* 变量提升部分
*/
// 把变量 myname提升到开头,
// 同时给myname赋值为undefined
var myname = undefined
// 把函数showName提升到开头
function showName() {
    console.log('showName被调用');
}

/*
* 可执行代码部分
*/
showName()
console.log(myname)
// 去掉var声明部分,保留赋值语句
myname = '极客时间'

为什么会变量提升?
因为js 在执行的过程,会先编译,而变量提升是在编译阶段发生的,编译器会将声明的变量放在变量环境中,可执行的代码放在可执行代码中。记住一点,函数只有在运行时才会被编译

实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中
JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。

  • JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译

  • 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;
    在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。

  • 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。

showName() 
var showName = function() { console.log(2)}
function showName() { console.log(1)}
// 编译阶段
var showName  = undefined
function showName() { console.log(1)}
// 执行阶段
showName()  // 1
var showName = function() { console.log(2)}
  // 赋值之后showName 函数在引用就变了,如果后面再去执行showName()  就会打印出 2

从图中可以看出 编译后会产生2部分: 执行上下文 和 可执行代码

执行上下文是 JavaScript 执行一段代码时的运行环境。比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等

js 函数调用栈

  • 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁


5e2bb65019053abfd5e7710e41d1b405.png

调用栈

avaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。


var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

执行上面代码的时候是如何?

  • 创建全局上下文,并将其压入栈底。
1.png
  • 全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2;


    2.png
  • 第二步是调用 addAll 函数。当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中。
    执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。

3.png
  • 第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈,
4.png
  • 当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。


    6.png
  • addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了


    1.png

调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

栈溢出

调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢

作用域:

作用域是·在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

全局作用域:对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
函数作用域: 就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁

ES6 之前是不支持块级作用域的

为什么要引入块级作用域?变量提升所带来的问题?

1、变量容易在不被察觉的情况下被覆盖掉

2、本应销毁的变量没有被销毁

function foo(){ 
for (var i = 0; i < 7; i++){ }
 console.log(i);
}
foo()
// 但是在 JavaScript 代码中,i 的值并未被销毁,所以最后打印出来的是 7
//这同样也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁

为了解决变量提升带来的缺陷,ES6 引入了let const 的关键字
从而使 JavaScript 也能像其他语言一样拥有了块级作用域。

js 如何支持块级作用域的


function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()
  • 第一步是编译并创建执行上下文
    1、var 声明放在 变量环境中,
    2、let 声明的变量,在编译阶段放在词法环境中。
    3、在函数的作用域内部,通过 let 声明的变量并没有被存放到词法环境中(执行的时候才会被)

  • 第二步继续执行代码

1、进入代码块执行,作用域块中let 声明的变量,单独放在词法环境的一个区域,这个区域中的变量并不影响作用域块外面的变量

函数只会在第一次执行的时候,在编译,所以编译时,变量环境和词法环境顶层的。
当执行到块级作用域的时候,块级作用域中通过let和const申明的变量会被追加到词法环境中,当这个块执行结束之后,追加到词法作用域的内容又会销毁掉。

7e0f7bc362e0dea21d27dc5fb08d06fa.png
  • 变量查找的过程


    06c06a756632acb12aa97b3be57bb908.png

块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域

function test(){
    console.log(a)
        let a = 7;
}
test()
// Uncaught ReferenceError: Cannot access 'a' before initialization
    at test (<anonymous>:2:17)
    at <anonymous>:5:1

意思是虽然该变量已经在词法环境中了,但是还没有被赋值,所以不能使用! 这也是JavaScript语法层面的标准,JavaScript引擎是按照标准来实现的。
因为let和const有个暂时性死区的设置,所以必须的先声明,再使用

`块级变量(let,const)会存在提升现象(网上的不存在其实是不严谨的).

但是与var的变量提升不同的是,其变量提升不会做词法绑定(语法规定,暂时性死区,可以理解为不会初始化,且不赋值则无法使用),而var会初始化为undefined。`

执行函数的时候才会有编译,抽象语法树AST 在进入函数阶段就生成了,并且函数作用域就已经明确了,所以进入块级作用域不会有编译过程,只不过通过let或者const声明的变量会在进入块级作用域的时被创建,但是在该变量没有赋值之前,引用该变量JavaScript引擎会抛出错误---这就是“暂时性死区”

闭包:

理解闭包前提 先知道什么是作用域链

作用域链:

  • 其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer
  • 当一代码引用变量,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,-> JavaScript 引擎会继续在 outer 所指向的执行上下文中查找
  • 我们把这个查找的链条就称为作用域链
87d8bbc2bb62b03131802fba074146f7.png

本来以为 先在当前执行上下文中,查找变量 myName,然后没有找到,再 foo 中查找,后来发现不是这样的; 为什么呢? 那么先说下词法作用域

词法作用域:

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符

词法作用域就是根据代码的位置来决定的

216433d2d0c64149a731d84ba1a07739.png

因为 JavaScript 作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域。

foo 函数调用了 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?

foo 和 bar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系

比如:
下面函数 根据词法作用域 查找


87d8bbc2bb62b03131802fba074146f7.png

首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域

闭包:

当通过调用一个外部函数 返回一个内部函数的后,即使外部函数执行结束,内部函数引用外部函数的变量依然保存在闭包中。我们称这个变量的集合为闭包。

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

闭包其实是在编译阶段,一个函数的运行需要经过编译阶段,创建执行上下文和执行代码块,然后开始执行。

  • 1、编译器会对foo 函数 进行快速的词法扫描
  • 2、遇到 getName 其引用了外部函数的test1,js引擎判断其是个闭包。
  • 3、创造一个变量环境 foo(closure) 堆空间,并且在栈空间进行引用。
  • 4、扫描到setName 也是同样的道理
  • 5、函数foo出栈,closure(foo)还是处在被引用状态,这就是闭包。
d5587b76427a56c5f0b0571e4264b7ef.png
  • innerBar 是一个对象,包含了 getName 和 setName 的两个方法(通常我们把对象内部的函数称为方法)
  • 根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量
  • foo 函数执行完成之后,其执行上下文从栈顶弹出了
  • 但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。
ee7c1ca481875ad4bdeb4383bd1f883f.png

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包

50e4ba60fc7e420e83b35b95e379b246.png

当执行到 bar.setName 方法中的myName = "极客邦"这句代码时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量

闭包怎么回收

如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;
但如果这个闭包以后不再使用的话,就会造成内存泄漏.

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存

  • 如果这个闭包一直会使用,它可以作为一个全局变量存在;
  • 如果使用的频率不高,而且又很大,尽量作为一个局部的变量存在;
function foo(){
var a=2;
return function fn(){
console.llog(a)
}
}
let func = foo();
func();

闭包使得函数可以继续访问定义时的词法作用域。拜 fn 所赐,在 foo() 执行后,foo 内部作用域不会被销毁。

闭包的作用:

  • 能够访问函数定义时所在的词法作用域(阻止其被回收)
  • 私有化变量
WechatIMG60092.png
  • 模拟块级作用域
WechatIMG541.png
  • 创建模块


    创建模块

闭包的使用场景:

1、for循环内部函数需要获取计数器

<body>
    <ul>
        <li>link1</li>
        <li>link2</li>
        <li>link3</li>
        <li>link4</li>
        <li>link5</li>
    </ul>
</body>
<script type="text/javascript">
var ali = document.querySelectorAll("li");
for(var i=0;i<ali.length;i++){
    (function(i){ 
        ali[i].onclick = function(){ 
            console.log(i); 
        }
    })(i);
}
</script>

2、 采用函数引用方式的setTimeout调用

//原生的setTimeout传递的第一个函数不能带参数
    setTimeout(function(param){
        alert(param)
    },1000)


    //通过闭包可以实现传参效果
    function func(param){
        return function(){
            alert(param)
        }
    }
    var f1 = func(1);
    setTimeout(f1,1000);

3、闭包应用场景之封装变量

 //用闭包定义能访问私有函数和私有变量的公有函数。
    var counter = (function(){
        var privateCounter = 0; //私有变量
        function change(val){
            privateCounter += val;
        }
        return {
            increment:function(){   //三个闭包共享一个词法环境
                change(1);
            },
            decrement:function(){
                change(-1);
            },
            value:function(){
                return privateCounter;
            }
        };
    })();

    console.log(counter.value());//0
    counter.increment();
    counter.increment();//2
    //共享的环境创建在一个匿名函数体内,立即执行。
    //环境中有一个局部变量一个局部函数,通过匿名函数返回的对象的三个公共函数访问。

常见面试题:

第一道题分析:

 function fun(n,o){
    console.log(o);
    return {
        fun:function(m){
            return fun(m,n);
        }
    };
 }

 var a = fun(0);a.fun(1);a.fun(2);a.fun(3);
 var b = fun(0).fun(1).fun(2).fun(3);
 var c = fun(0).fun(1);c.fun(2);c.fun(3);
 //问:三行a,b,c的输出分别是什么?

调用fun

function fun(n,o){
    console.log(o);
    return {
        fun:function(m){
            return fun(m,n);
        }
    };
 }

 var a = fun(0);

console.log(o); 输出undefined
var a = fun(0); 那a是值是什么,是fun(0),返回的那个对象

{
    fun:function(m){
        return fun(m,0);
    }
}

var a=fun(0),传入一个参数0,那就是说,函数fun中参数 n 的值是0了,而返回的那个对象中,需要一个参数n,而这个对象的作用域中没有n,它就继续沿着作用域向上一级的作用域中寻找n,最后在函数fun中找到了n,n的值是0,这段话是本文的重点, 明白这段,那问题就容易解决了。
a 是:

{
    fun:function(m){
        return fun(m,0);
    }
}

a.fun(1) 相当于:

{
    fun:function(1){
        return fun(1,0);
    }
}
// 这时候 a.fun(1) 返回的是 fun(1,0) 所以打印出来 0
 function fun(n,o){ //n的值为1,o的值为0
        console.log(o);
        return {
            fun:function(m){
                return fun(m,n);//n的值为1
            }
        };
}
fun(1,0);  //输出0,并返回一个对象,这个对象有一个fun的方法,这个方法调用后,会返回外层fun函数调用的结果,并且外层函数的第二个参数是 n 的值,也就是1 

那么a.fun(2)

{
    fun:function(2){
        return fun(2,0);
    }
}
// a.fun(2) 返回的是 fun(2,0)

function fun(n,o){ //n的值为2,o的值为0
        console.log(o);
        return {
            fun:function(m){
                return fun(m,n); //n的值为2
            }
        };
}
fun(2,0);  //输出0,并返回一个对象,这个对象有一个fun的方法,这个方法调用后,会返回外层fun函数调用的结果,并且外层函数的第二个参数是 n 的值,也就是2 

var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
b和a的不同在于, var a = fun(0); 之后一直用的是a这个对象,是同一个对象,而b每次用的都是上次返回的对象

var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
//undefined 0 0 0 
  
var b = fun(0).fun(1).fun(2).fun(3);
//undefined 0 1 2
  
var c = fun(0).fun(1); c.fun(2); c.fun(3);
//undefined 0 1 1

第二道题分析:

var name="global";
function foo(){
    console.log(name);
}

function fooOuter1(){
    var name="local";
    foo();
}
fooOuter1();//输出global 而不是local,并且和闭包没有任何关系

function fooOuter2(){
    var name="local";
    function foo(){
        console.log(name);
    }
    foo();
}
fooOuter2();//输出local 而不是global,在函数声明是name变量作用域就在其外层函数中,嗯嗯就是闭包~

这里就要说到JS的词法作用域,JS变量作用域存在于函数体中即函数体,并且变量的作用域是在函数定义声明的时候就是确定的,而非在函数运行时

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符

第三道:

 function foo(i) {
            if(i  == 3) {
                return;
            }
            foo(i+1);
            console.log(i);
  }
        foo(0);
// 2 1 0

ECS栈顶为foo(3)的的上下文,直接return弹出后,栈顶变成foo(2)的上下文,执行foo(2),输出2并弹出,执行foo(1),输出1并弹出,执行foo(0),输出0并弹出,关闭浏览器后全局EC弹出,所以结果为2,1,0。

参考: 极客时间 李兵《浏览器工作原理》

相关文章

  • Node.js 有什么弊端

    《深入浅出 Node.js》阅读随笔 此文为《为什么选择 Node.js》姊妹篇,简单聊一下 Node.js 的缺...

  • Node.js 有必要写测试用例吗?

    《深入浅出 Node.js》阅读随笔 先说结论:很有必要!不单是 Node.js,除非是作为体验或者 Demo 演...

  • 异步编程的困惑

    《深入浅出 Node.js》阅读随笔 众所周知,Node.js 虽然也有部分同步编程的方式,但主要还是以后异步编程...

  • 发布/订阅模式

    《深入浅出 Node.js》阅读随笔 Node.js 下使用发布/订阅的模式写程序,可以很优雅的解藕业务逻辑,尤其...

  • Android与JS 交互

    这篇算是一个随笔记录吧,最近写了一个简单的js,并要与原生进行交互,之前只知道如何与js交互,但不知道js侧是如何...

  • js随笔

    validatePhone createAsyncValidate 对象key 值替换 需求:后端返回的数据和前端...

  • JS随笔

    JavaScript中数据类型 number string boolean undefined object nu...

  • js随笔

    forEach 中不能有异步代码 否则会直接跳过异步, 先执行后续代码在执行循环内的异步代码块查阅资料后发现大概是...

  • js 随笔

    js 执行机制: js执行上下文: 只有理解了js 执行上下文才能更好的理解 js变量提升以及 作用域和闭包 所谓...

  • JS随笔

    1、join() 方法用于把数组中的所有元素放入一个字符串; 2、split() 方法用于把一个字符串分割成字符串...

网友评论

      本文标题:js 随笔

      本文链接:https://www.haomeiwen.com/subject/cdsfkhtx.html