从头梳理JS基础(二)从头开始梳理函数执行的整个过程

本文主要梳理JS 函数执行的整个过程,包括执行上下文,作用域链,内存空间,闭包,this指向和call,apply,bind等,会持续补充更新哦!

执行上下文

评估和执行 JavaScript 代码的环境的抽象概念。

  • 全局执行上下文— 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中(一个程序中只会有一个全局执行上下文)。

  • 函数执行上下文— 每当一个函数被 调用 时, 都会为该函数创建一个新的上下文(函数上下文可以有任意多个)。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。

  • Eval 函数执行上下文— 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,暂不讨论。

生命周期

创建阶段

全局上下文

  • 生成变量对象:全局对象(浏览器的情况下为window )

  • 建立作用域链:全局对象

  • 确定this指向:设置 this 的值等于全局对象(var === this. === winodw.)。

函数上下文

  • 生成变量对象:用活动对象(activation object, AO)来表示变量对象(活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化)。

  • 建立作用域链 :
    • 函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中。

    • 当函数激活时,进入函数上下文,创建 VO/AO后,就会将活动对象添加到作用链的前端。

    • Scope (作用域链)= [AO].concat([[Scope]]);

  • 确定this指向:this 永远指向最后 调用 它的那个对象(参见后文)

执行阶段

进入执行上下文

这时候还没有执行代码, 变量对象 会加入:

  • 函数的所有形参 (如果是函数上下文)
    • 由 名称和对应值 组成的一个变量对象的属性被创建

    • 没有 实参 ,属性值设为** undefined**

  • 函数声明
    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建

    • 如果变量对象已经存在 相同名称 的属性,则 完全替换 这个属性

  • 变量声明
    • 由**名称和对应值(undefined)**组成一个变量对象的属性被创建(var);

    • 如果 变量名称 跟已经声明的 形参 或 函数 相同,则变量声明不会干扰已经存在的这类属性

    • 变量声明提升:可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误(未初始化)。

代码执行

  • 顺序执行代码,根据代码,修改变量对象的值

总结一下函数执行上下文的整个过程

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
复制代码

执行过程如下:

  1. checkscope 函数被创建,保存作用域链到 内部属性[[scope]]

  2. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

  3. checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

  4. 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

  5. 第三步:将活动对象压入 checkscope 作用域链顶端

  6. 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

  7. 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

执行上下文栈

存储代码运行时创建的所有执行上下文。

  • 当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

  • 引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

作用域和作用域链

  • 作用域是指程序源代码中定义变量的区域。

  • 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

  • JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

静态和动态作用域

  • 词法作用域, 函数的作用域在函数定义的时候就决定了 。

  • 动态作用域,函数的作用域是在函数调用的时候才决定的。

作用域链

  • 当查找变量的时候,会先从当前上下文的变量对象中查找。

  • 如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。

  • 这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

  • 作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回 undefined ;但查找的属性在作用域链中不存在的话就会抛出 ReferenceError 。

内存空间

JS内存空间分为 栈(stack) 、 堆(heap) 、 池(一般也会归类为栈中) 。 其中  存放变量,  存放复杂对象,  存放常量,所以也叫常量池。

内存生命周期

  • 分配你所需要的内存

  • 使用分配到的内存(读、写)

  • 不需要时将其释放、归还

垃圾回收

JS有自动垃圾收集机制,常用 标记清除 算法来找到哪些对象是不再继续使用的,当将变量设为null时释放引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。

  • 局部变量:局部作用域中,函数执行完毕,局部变量没有存在意义,垃圾收集器很容易做出判断并回收。

  • 全局变量:全局变量什么时候需要自动释放内存空间则很难判断,所以在开发中尽量 避免 使用全局变量。

常见垃圾回收算法

  • 引用计数(现代浏览器不再使用):
    • 看一个对象是否有指向它的 引用 。如果没有其他对象指向它了,说明该对象已经不再需要了。

    • 循环引用:如果两个对象相互引用,尽管他们已不再使用,但是垃圾回收器不会进行回收,最终可能会导致内存泄露。

  • 标记清除(常用):
    • 从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象, 保留 。那些从根部出发无法触及到的对象被标记为 不再使用 ,稍后进行回收。

    • 无法触及的对象包含了没有引用的对象这个概念,但反之未必成立。

常见的内存泄漏

1、意外的全局变量

function foo(arg) {
    a = "this is a hidden global variable"; //未使用var定义
    this.b = "potential accidental global"; //this指向全局
}
复制代码

解决方法:

在 JavaScript 文件头部加上 'use strict' ,使用严格模式避免意外的全局变量,此时 上例中的this指向 undefined 。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。

2、被遗忘的计时器或回调函数

  • 必须手动终止定时器

  • 现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用 removeEventListener 了。

3、脱离 DOM 的引用

如果把DOM 存成字典(JSON 键值对)或者数组,此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。那么将来需要把两个引用都清除。

闭包

  • 闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。闭包的外部作用域是在其 定义 的时候已决定,而不是执行的时候。

  • 闭包中的变量并不保存中栈内存中,而是保存在堆内存中,这也就解释了函数之后之后为什么闭包还能引用到函数内的变量,这些被引用的变量直到闭包被销毁时才会被销毁。

  • 闭包使得 timer 定时器,事件处理,AJAX 请求等异步任务更加容易,可以通过闭包来达到封装性。

  • 能不能访问关键看在哪里定义,而不是在哪里调用, 调用方法的时候,会跳转到定义方法时候的环境里,而不是调用方法的那一行代码所在的环境。

  • 闭包引起的内存泄露那都是因为浏览器的gc问题(IE8以下为首)导致的,跟js本身没有关系,所以,请不要再问js闭包会不会引发内存泄露了

  • 闭包只存储外部变量的引用,而不会拷贝这些外部变量的值。var 只有函数作用域 let,coast有函数作用域和块作用域。

问一个问题

下面的两段代码中, checkscope() 执行完成后,闭包 f 所引用的自由变量 scope 会被垃圾回收吗?为什么?

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

checkscope()();  
复制代码
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope(); 
foo();    
复制代码

第一段中自由变量特定时间之后回收:执行完毕后出栈,该对象没有绑定给谁,从Root开始查找无法可达,此活动对象一段时间后会被回收 第二段中自由变量不回收:此对象赋值给 var foo = checkscope(); ,将foo压入栈中,foo指向堆中的f活动对象,对于Root来说可达,不会被回收。

如果想让第二段中自由变量回收,要怎么办?

foo = null;,把引用断开就可以了。

this指向

this 永远指向 最后调用它的那个对象

this的值不会被保存在作用域链中,this的值取决于函数被调用的时候的情景(也就是执行上下文被创建时确定的)。

判断函数上下文中this的绑定对象

  • new绑定:作为一个构造函数,this绑定到 新创建的对象 ,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。

  • 显示绑定(call,apply,bind):
    • call()、apply()--this指向绑定的对象上

    • bind()--this将永久地被绑定到了bind的第一个参数

  • 隐式绑定:this指向**调用函数的对象,**由上下文对象调用时,绑定到上下文对象

  • 默认绑定: 非严格模式情况下,this 指向 window(全局变量), 严格模式下,this指向 undefined

  • 箭头函数--所有的箭头函数都没有自己的this
    • 箭头函数不绑定this,箭头函数中的this相当于普通变量。

    • 箭头函数的this寻值行为与普通变量相同,在作用域中逐级寻找。

    • 箭头函数的this无法通过bind,call,apply来 直接 修改(可以间接修改)。

    • 改变作用域中this的指向可以改变箭头函数的this。

    • eg. function closure(){()=>{//code }} ,在此例中,我们通过改变封包环境 closure.bind(another)() ,来改变箭头函数this的指向

  • 作为一个DOM事件处理函数--this指向触发事件的元素,也就是始事件处理程序所绑定到的DOM节点。

  • 立即执行函数(function() {})()中的this指向的window对象,因为完整写法就是window.(function() {})()

new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

call,apply,bind

call,apply,bind三者之间的区别

  • 三者都是用来改变函数的this指向

  • 三者的第一个参数都是this指向的对象

  • bind是返回一个绑定函数可稍后执行,call、apply是立即调用

  • 三者都可以给定参数传递

  • call和bind给定参数需要将参数全部列出,apply给定参数数组

模拟一个call

//ES6实现
Function.prototype.myCall=function(context){
    context=context || window  //当参数为null时指向window
    var args=[...arguments].slice(1)//将类数组对象转为数组并截取从1到结尾的参数
    var fn = Symbol() //设定fn为唯一属性
    context[fn]=this //fn绑定当前函数
    var result=context[fn](...args) //传入参并执行函数,考虑有返回值的情况
    delete context[fn] //删除fn
    return result //返回return值
}
//ES3实现
Function.prototype.myCall=function(context){
    context=context || window
    var args=[]
    for(var i=1;i<arguments.length;i++){ //for循环取参数数组
        args.push(arguments[i])
    }
    context.fn=this
    var result=eval('context.fn('+args+')') //eval解析参数列表
    delete context.fn
    return result
}
复制代码

模拟一个apply

apply和call的区别是call需要列出所有参数,而apply传入一个参数数组

//ES6实现
Function.prototype.myApply=function(context){
    context=context || window
    var args=arguments[1]||[] //与call不同的地方是直接传入一个参数数组,获取该数组
    var fn = Symbol();
    context[fn]=this
    var result=context[fn](...args)
    delete context[fn]
    return result
}
//ES3实现
Function.prototype.myApply=function(context){
    context=context || window
    var args=arguments[1]||[] //与call不同的地方是直接传入一个参数数组,获取该数组
    context.fn=this
    var result=eval('context.fn('+args+')')
    delete context.fn
    return result
}
复制代码

模拟一个bind

  • 1、可以指定 this

  • 2、返回一个函数

  • 3、可以传入参数

  • 4、柯里化

Function.prototype.myBind = function (context) {
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var self = this
    var args1 = [...arguments].slice(1)
    var fn = function () { };
    var newContext = function () {
        var args2 = [...arguments]
        return self.myCall(this instanceof fn ? this : context, ...args1, ...args2);
    }
    if (this.prototype) {
        fn.prototype = this.prototype
    }
    newContext.prototype = new fn();
    return newContext;
}
复制代码

疯狂自测一波

Q1:判断下面两段代码分别输出什么?

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
复制代码
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
复制代码

两段代码都会打印: local scope 。 JavaScript采用的是词法作用域,函数的作用域基于函数 创建 的位置。

Q2:下面 两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
复制代码
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
复制代码

执行上下文栈的变化不一样

//模拟第一段代码:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
//模拟第二段代码:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
复制代码

Q3:判断下面两段代码分别输出什么?

function foo() {
    console.log(a);
    a = 1;
}
foo(); 
复制代码
function bar() {
    a = 1;
    console.log(a);
}
bar(); 
复制代码

第一段会报错: Uncaught ReferenceError: a is not defined ( "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中) 第二段会打印: 1 (执行 console 的时候,全局对象已经被赋予了 a 属性)

Q4:判断下面这段代码输出什么?

console.log(foo);
function foo(){
    console.log("foo");
}
var foo = 1;
复制代码

会打印函数,而不是 undefined  (如果 变量名称 跟已经声明的 形参 或 函数 相同,则变量声明 不会干扰 已经存在的这类属性)

Q5:判断下面两段代码分别输出什么?

var foo = function () {
    console.log('foo1');
}
foo(); 
var foo = function () {
    console.log('foo2');
}
foo(); 
复制代码
function foo() {
    console.log('foo1');
}
foo(); 
function foo() {
    console.log('foo2');
}
foo(); 
复制代码

第一段会打印:foo1 和 foo2 变量声明提升 (提升为undefined,边执行边赋值) 第二段会打印:foo2 和 foo2 函数声明提升 (函数提升,当重复时后一个会对前一个进行覆盖)

Q6:判断下面这段代码输出什么?

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x)
console.log(b.x)
复制代码

a.x:undefined b.x:{n: 2} 原因:

  • .
    =
    a.x
    {n: 1}
    {n: 1, x: undefined}
    b.x
    
  • 2、赋值操作是 从右到左 ,所以先执行 a = {n: 2} , a 的引用就被改变了,然后这个返回值又赋值给了 a.x , 需要注意 的是这时候 a.x 是第一步中的 {n: 1, x: undefined} 那个对象,其实就是 b.x ,相当于 b.x = {n: 2}

Q7:判断下面两段代码的this对象是什么及输出什么?

var person = {
  name: "personName",
  getName: function(){
    return this.name;
  }
}
console.log(person.getName());
复制代码
var name = "windowName";
var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
var getName = person.getName;
console.log(getName());
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值