闭包的底层原理及相关作用域链延展

84 阅读5分钟

作用域

作用域是指程序源代码中定义的范围,分为全局作用域和局部作用域也叫函数作用域

作用域规定了如何设置变量,也就足确定当前执行代码对变量的访间权限 函数作用域采用词法作用域,也就是静态作用域

  • 所谓词法作用域就是在函数定义的时候,就已经确定了
var value = 1;
// 定义的时候就确定了作用域
function foo() {
    console.log(va1ue);
}
function bar() {
    var value = 2;
    foo();
}
bar(); // 1
  • 变量对象

变量对象是当前代码段中,所有的变量(变量函数形参arguments)组成的一个对象

变量对象是在执行上下文中被激活的,只有变量对象被激活了, 在这段代码中才能使用所有的变量

变量对象分为全局变量对象局部变量对象

全局简称为Viriable Object VO 函数由于执行才被激活称为Active Object AO

var value = 1;
function foo() {
    console.log(va1ue);
}
function bar() {
    var value = 2;
    foo();
}
bar(); // 1

image.png

作用域链

在js中,函数存在-个隐式属性[[scopes]],这个属性用来保存当前函数的执行上下文环境,由子在数据结构上是链式的,因此也被称作是作用域链,我们可以把它理解为一个数组

可以理解为是一系列的AO对象所组成的一个链式结构

scopes 存的就是VO的集合

function a() {}
consale.dir(a) // 打印结构

image.png

  • 当函数被调用后
function a() {
  cosole.log(a)
}
a()

image.png

因此我们可以得出一个结论: [[scops]]属性在函数声明时产生,在函数调用时更新

即:在函数被调用时,将该图数的A0对象压入到[[scopes]]中

作用域链的作用

作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的

最直观的表现就是:

  • 函数内部可以访问到函数外部声明的变量
var a=100
function fn(){
  console.log(a)
}
fn() // 100
  • 函数外部访问不到函数内部的变量
function fn() {
  var a=100
}
fn()
console.1og(a) // a is not defined

画出下面代码执行的作用域链

var global
function a() {
  var aa = 123
  function b() {
    var bb = 234
 }
  b()
}
a()
  • 第一步:a函数生成

image.png

  • 第二步:a函数调用

image.png

  • 第三步:b函数生成(a函数调用导致b函数生成)

image.png

  • 第四步:b函数调用

image.png

函数执行完就销毁, 所谓销毁就是断掉图中的线

思考a函数调用和b函数生成是不是同一个作用域链

  • 是的, 下面代码验证
var global
function a() {
    //到这个里面来了
    var aa = 123
    // b函数定义
    function b() {
        //到这里面来了
        var bb = 234
        aa = 0
    }
    // b执行
    b()
    console.log(aa);
}
// a执行
a()
//打印aa的结果,如果是00,说明是同一个没有分离
  • 那么就有了下面的图解

image.png

看看闭包

var global
function a() {
    //到这个里面来了
    var aa = 123
    // b函数定义
    function b() {
        console.log(aa)
    }
    return b
}
// a执行
var res = a()
res()
  • 底层原理结合图9讲解.

a执行完 - 销毁 - 断掉a的线,但b定义的时候,其的作用域链还在!!!

a()执行完,把b函数保存到了外面。

  • 造成内存泄漏的前提是:内部的函数只有被return出来,保存到外部去执行的时候才会造成内存泄漏。

经过上述的讲解那么谈谈闭包的定义

  • 就是能够读取其他函数内部变量的函数

闭包的实战应用

  1. 回调函数
function add(num1, num2, callback) {
    var sum = num1 + num2;
    if (typeof callback === 'function') {//类型判断,确认是一个函数
        callback(sum);
    }
}
add(1, 2, function (sum) {
    console. 1og(sum);
})

  1. 手写js的方法-手写bind方法
let foo={
  name: 'jill'
}
function getName() {
    console.1og(this.name)
}
Function.prototype.myBind = function(obj) {
    //将当前函数的this指向目标对象
    let_ self = this
    return function() {
        return _self.call(obj)|
    }
}
let getFooName = getName.myBind(foo)
getFooName()
  1. 防抖节流函数
  2. 单例模式
  3. 定时器传参
function fn(a) {
    return function() {
        console.log(a)
    }
}
setTimeout(fn(123),1000)

6.利用闭包判断数据类型

function isType(type) {
    return function (target) {
        return `[object ${type}]` === object.prototype.tostring.ca1l(target)
    }
}

const isArray = isType('Array')
console.log(isArray([1, 2, 3])); // true
console.log(isArray({}));
  1. 封装私有变量和函数
function createPerson(name) {
  var age = 0;
  return {
    getName: function() {
      return name;
    },
    getAge: function() {
      return age;
    },
    setAge: function(newAge) {
      age = newage;
    }
  };
}
var person = createPerson("John");
console.log(person.getName(); // "John"
console.log(person.getAge()); // 0
person.setAge(30);
console.log(person. getAge()); // 30
  1. 高阶函数
  • 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
  • 实现对一个数组的求和
function sum(arr) {
  return arr.reduce(function (x, y) {
    return x + y;
  });
}
sum([1, 2, 3, 4, 5]); // 15
  • 但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办? 可以不返回求和的结果,而是返回求和的函数!
function 1azy_sum(arr) {
    var sum = function () {
    return arr.reduce(function (x, y) {
        return x + y;
    });
    return sum;
}
var f = 1azy_sum([1, 23, 4, 5]);
  1. 迭代器(执行一次函数往下取一个值)
var arr = ['aa','bb','cc'];
function incre(arr){
    var i=0;
    return function(){
        //这个函数每次被执行都返回数组arr中i下标对应的元素
        return arr[i++] || ' 数组值已经遍历完';
    }
}
var next = incre(arr);
console.1og(next()); //aa
console.1og(next()); //bb
console.log(next()); //cc
console.log(());      //数组值已经遍历完
  1. 缓存

比如求和操作,如果没有缓存,每次调用都要重复计算,采用缓存已经执行过的去查找,查找到了就直接返回,不需要重新计算

var fn = (function(){
    var cache = {};//缓存对象
    var calc = function(arr){//计算函数
        var sum = 0;
        // 求和
        for(var i=0;i<arr.1ength;i++){
            sum+=arr[i];
        }
        return sum;
    }
    return function(){
        var args = Array.prototype.slice.call(arguments ,0) ;//arguments转换成数组
        var key-args.join(",");//将args用逗号连接成字符串
        var result,tsum = cache[key];
        if(tsum){//如果缓存有
            console.1og('从缓存中取: ',cache)//打印方便查看
            result = tsum;
        }else{
            //重新计算,并存入绥存同时赋值给result
            result = cache[key]=calc(args);
            console.1og('存入缓存: ' ,cache)//打印方便查看
    }
    return result;
    }
})();
fn(1,2,3,4,5);
fn(1,2,3,4,5);
fn(1,2,3,4,5,6);
fn(1,2,3,4,5,8);
fn(1,2,3,4,5,6);