JS函数详解(二)

脑图

在这里插入图片描述

1 作用域与作用与链


1.1 分类


  1. 全局作用域:定义在全局,所有的变量都能访问到
  2. 局部作用域:定义在函数体内,只有函数内的语句才能访问到
  3. 块级作用域:是ES6之后出现的,使用 constlet ,会使{}形成一个封闭的作用域

1.2 概念


形象的比喻作用域,可以拿学校来做比喻

  • 全局作用域:就相当于整个学校,定义在全局作用于中的变量,就如同学校通过广播站传递的信息,所有的代码(学生),都能看到
  • 局部作用域和块级作用域,去们就相当于一个个的教室,在教室中发布的消息,只有教室中的学生能看到
var a = 10,
    b=20
function fn(x){
    var a = 100,
        c = 200
    console.log("调用fn()",a,b,c,x)
    // console.log("访问子作用域的变量", d)  报错:d is not defined
	// 自作用于能访问到副作用域的变量,父作用域不能访问子作用域的变量,且相邻作用域也不能相互访问
    function bar (x){
        var a = 1000,
            d = 2000
        console.log("调用bar()",a,b,c,d,x)
    }
    bar(100)
    bar(200)
}
fn(10)

输出

在这里插入图片描述

1.3 作用域链


嵌套作用域,找寻变量的过程,类似于一条链,就把他叫做作用域链

在这里插入图片描述

如上图中,我们在嵌套函数fn2内部打印变量a

  1. 他会首先在自身的作用域中寻找a,找到则使用
  2. 如果没有找到,则会去父级作用域中寻找,找到返回
  3. 如果没有找到,再去上上级作用中寻找,知道找到全局作用域
  4. 如果全局也没有找到,则报错a is not defind

2 执行上下文与执行上下文栈


2.1 变量提升


JS中的变量提升分为普通变量提升和函数变量提升

变量提升

  • 使用关键字var,声明的变量,会在定义语句之前就可以访问
  • 他的值被赋值为undefined

有变量提升

var a = 1
function fn (){
    console.log(a)
    // 用 var 声明的变量偶变量提升
    var a = 2
    }
fn() // undefined

无变量提升

var a = 1
function fn (){
    console.log(a)
    a = 2
}
fn() // 1

函数提升

  • 使用关键字function声明的函数,在声明之前就可以直接调用
  • 变量指向的值是定义的函数体

有函数提升

sayName() // hello world
function sayName(){
    console.log('hello world')
}

无函数提升

sayName() // 报错:sayName is not a function
var sayName = function(){
    console.log('hello world')
}

2.1.1 变量提升和函数提升的优先级


先执行变量提升,再执行函数提升,函数提升会覆盖变量提升

例题一

function a(){}
var a
console.log(typeof a) // function

例题二

if(!(b in window)){
    var b = 1
}
console.log(b) // undefined
  • if的花括号起到的是分类的作用(一个花括号内),在这里b属于全局作用域 点击

  • 因为使用var进行的变量声明,会有变量的提升,不会执行条件句中的语句

例题三

var c = 1
function c(c){
    console.log(c)
    var c = 3
    }
c(2) // 报错 c is not a function
  • 首先进行变量的提升在进行比变量的赋值,以上例题三可以写成这个样子
var c ;
function c(c){
    console.log(c)
}
c = 1
c(2)
  • 此时的c并不是一个函数而是一个常量

2.2 执行上下文


执行上下文是变量提升的根本原因

2.2.1 概念


当代码运行时,会产生一个对应的执行环境。在这个环境中,所有的变量会被事先提出来,然后代码从上往下开始执行,就叫做执行上下文

2.2.2 分类


代码可以分为全局代码和局部代码,所以代码的执行环境可以分为

  • 全局环境
  • 函数环境
  • eval环境(不常用,这里不做讨论)

所以上下文可以分为全局执行上下文函数执行上下文,上下文指的就是一种环境

2.2.3 代码的执行流程


在准备执行代码前会首先创建相应的上下文(执行环境),执行全局代码会创建全局上下文环境,函数调用,执行函数体中的代码的时候,会创建函数执行上下文

2.2.3.1 全局代码的执行流程
  1. 确定window为全局执行上下文

对于变量和this做处理(声明提前)

  1. 收集var定义的全局变量,并赋值为undefined
  2. 收集function定义的全局函数变量,并赋值函数体,添加为window的方法
  3. this赋值为window

当全局代码做好了上面四步之后,才会一步步执行代码

console.log(a) // undefined
console.log(fn) // fn的函数体
console.log(this) // window

var a = 1
function fn(){
    console.log()
}

console.log(a) // 1
2.2.3.2 函数代码执行流程

函数执行上下文的创建是在调用函数的一瞬间,开始执行函数代码之前创建的,这里的执行上下文与函数的作用与差不多,你可以把函数作用域理解为一个对象,就是函数执行上下文对象

过程

  1. 将形参变量赋值为实参,并添加为执行上下文的属性
  2. arguments赋值为实参列表,并添加问执行上下文属性
  3. 收集var定义的局部变量,赋值为undefined,并添加为执行上下文的属性
  4. 收集function定义的局部函数变量,并赋值函数体,添加为上下文的方法
  5. this赋值为调用函数的对象obj.fun(),则this就是obj

当执行完上面的5个步骤之后,变开始执行函数体内的代码

function fn(a,b){
    // 在这里就会有一个隐形的对象(执行上下文对象)和作用域差不多
    console.log(a,b) // 1,2
    console.log(arguments)// 实参列表
    console.log(sum) // undefined
    console.log(this) // window
    var sum = a + b
    function fun(){
    }
    console.log(sum)
}
fn(1,2)

2.3 执行上下文栈


在函数执行 JS引擎就会创建一个栈来存储管理所有的执行上下文对象,注意栈的结构遵循 LIFO原则:先进后出,在执行代码前执行前,相应的执行上下文栈已经建好了

2.3.1 流程与图示


在这里插入图片描述

  1. 当确定全局上下文对象window后,将其添加到栈中(压栈)
  2. 函数执行上下文创建后,将其添加到到栈中
  3. 当前函数执行完后,将栈顶的对象移除(出栈)
  4. 当所有代码都执行完后,栈中只会剩下window

例题

// 1. 执行代码前,将全局中的变量添加到全局上下文对象中,并压入栈中,第一张图
console.log(window)
var a = 10
var bar = function(x) {
    var b = 5
    foo(x + b) // 3. 在bar函数中执行fn函数,将fn压入栈中,bar代码还没有执行完,依旧在栈中
}
var fn = function(y) {
    var c = 5
    console.log(a + c + y)// 4. fn代码执行完,fn上下文环境销毁,变量被释放,同时bar代码执行完毕
}
bar(10) // 2. 调用bar函数,会将bar的执行上下文压入栈中,第二张图

在这里插入图片描述

2.3.2 执行上下文与作用域


作用域和执行上下文环境其实是很相似的,下面我们来说一说他们的联系和区别

2.3.2.1 联系
  • 上下文对象从属于所在的作用域,所在的作用域有什么,上下文环境中就会有什么
  • 全局作用域对应着全局上下文环境,函数作用域对应着函数上下文环境

例题

以下例题来说明上下文对象与作用域的区别

var a = 10,
    b = 20
function fn(x){
    var a = 100
    c = 300
    function bar(x){
        var a = 1000,
            b = 4000
        }
    bar(100)
    bar(200)
}
fn(10)

在这里插入图片描述

2.3.2.2 区别

创建时间

  • 除全局作用域外,每个函数都会创建自己的作用域,作用域在函数定义的时候就确定了,并不是在函数调用的时候
  • 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
  • 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建

状态

  • 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
  • 上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放

3 闭包


3.1 闭包的产生和查看


详情点击

3.2 常见的闭包和闭包的应用


3.2.1 常见的闭包


因为闭包的产生的条件中有一个是 嵌套的函数,所以我们常见的闭包都以两个函数存在

  1. 将一个函数作为另一个函数的返回值(这也是高级函数的基础)
function fn1(){
    var a = 2
    function fn2(){
        /* 嵌套的函数引用外部变量 */
        console.log(a)
    }
    return fn2
}
fn1()() //2 fn1调用返回一个函数,再次调用
  1. 将函数作为实参传递给另一个函数调用
function showDelay(msg,time){
    setTimeout(()=>{
        console.log(msg)
    },time)
}
showDelay('hello world',2000) // hello world

3.2.2 闭包的应用


经过作用域和执行上下文的学习我们知道了

  1. 函数执行完毕,由于执行上下文栈的销毁,函数体内的变量会被释放
  2. 因为函数作用域的存在,父级作用域无法访问子级作用域中的变量
3.2.2.1 闭包的作用
  1. 避免函数体内的变量执行完后被释放,延长变量的生命周期
    • 只要引用外部变量的嵌套内部函数不执行,上下文对象就不会销毁,变量一直存在
function fn1(){
    var a = 2
    function fn2(){
        a++
        console.log(a)
    }
    function fn3(){
        a--
        console.log(a)
    }
    return fn3
}
const fun = fn1()
fun() // 1
fun() // 0
fun() // -1
// 只要fn2不执行 a变量一直存在于内存中
  1. 函数外部可以访问内部变量
    1. 需要定义一个操作内部数据的函数
    2. 需要将操作内部数据的函数暴露给外边使用,这里有两种方式
      • return
      • 将方法添加到外部属性( w i n d o w window window)中,利用 I I E F IIEF IIEF

方式一

function moudle(){
    var msg = 'hello world'
    // 定义操作内部变量的函数
    function dothings(){
        console.log(msg.toUpperCase())
    }
    /* 返回给外部调用 */
    return {
        dothings
    }
}
const myMoudle = moudle() // 执行返回一个对象
myMoudle.dothings() // HELLO WORLD

方式二

(function(){
    var msg = 'HELLO WORLD'
    /* 1. 定义操作方法 */
    function dothing(){
        console.log(msg.toLowerCase())
    }
    /* 2. 将操作方法暴露出去(添加到外部属性中) */
    window.modlue = {
        dothing
    }
})()
modlue.dothing() // hello world

3.3 闭包生命周期


产生:因为变量的提升,在嵌套内部函数定义执行完的时候就产生了(不是在调用的时候)

死亡:嵌套的内部函数称为垃圾对象

function fn1(){
    // 产生闭包
    var a = 2
    function fn2(){
        a++
        console.log(a)
    }
    return fn2
}
const fun = fn1()
fun() //3
fun() //4
fun = null // 闭包死亡

3.4 闭包的缺点


3.4.1 缺点


  1. 函数执行完后,局部变量不能得到及时的释放,占用内存的时间会变长
  2. 容易造成内存泄露

3.4.2 关于内存溢出和内存泄露


3.4.2.1 内存溢出

当程序运行需要的内存超出了计算机的剩余内存的时候就会抛出内存不足的错误

var obj = {}
for(var i = 0; i < 10000000 ; i++ ){
    /* 把新建的数组添加到独享中 */
    obj[i] = new Array(1000000000)
    console.log('hello')
}
  • 当我们把上面代码上面代码放到浏览器中运行,同时打开任务管理器,查看内存,就会发现内存会上升
  • 如果数大到一定的程度,浏览器直接崩溃
  • 这是因为当我们使用new来新建数组的饿时候,他会在堆内存直接开辟一片空间,而当需要的空间超出了堆内存的时候,他就会报错

在这里插入图片描述

3.4.2.2 内存泄露

内存泄露并不会报错,他指的是变量或者对象占用的内存没有及时释放,但是当没有及时释放的内存过多的时候就会造成内存泄露

常见的内存泄露

  1. 意外的全局变量
  2. 没有及时清理的计时器或者回调函数
  3. 闭包

意外的全局变量

function fn(){
    // 声明的变量在全局中,不会被会释放
    a = 1
    console.log(a)
}
fn()

没有及时清理的计时器或者回调函数

const intervalId = setInterval(() => {
    console.log('1111')
})
clearInterval(intervalId) // 

3.4.3 解决


  1. 闭包能不使用就不使用
  2. 使用闭包及时释放
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值