多案例快速搞懂js闭包

2021-06-07  本文已影响0人  景阳冈大虫在此

前言

闭包是个容易老马失蹄的概念,让我们先从案例入手来了解闭包是什么。

案例

案例一

case 1.1

这是个真实案例

var greet = function (props) {
  return (name) => console.log(props.content, name);
};

class Greeter {
  constructor(props) {
    this.props = props;
  }
  task = greet(this.props);
}

const instance = new Greeter({ content: "hello" });
instance.task("jack");
instance.props.content = "hi";
instance.task("jack");

猜一下会输出什么?
实际上,这段程序会抛出TypeError的错误

结果
调用栈
观察现象
观看调用栈可知,Greeter的方法task在类声明时,就已经被赋值了一个闭包,而这个闭包内部的props尚未赋值。
当new 实例后,即使this.props发生改变,也不会影响到闭包内部的props。

case 1.2

function interview(){
  setTimeout(()=>{
    throw new Error('aaa');
  },0)
}

function task(){
  try{
    interview();
    throw new Error('b');

  }catch(e){
    console.log(e);
  }
}

task();
结果 调用栈

在setTimeout的回调函数里的error,因为调用时是在另一个调用栈中,实际上并没有被在task里的那个try catch包裹,所以这个错误没有被捕获。

案例二

var greet = function (props) {
  return () => console.log(props.content);
};
var props = { content: "hello" };
var task = greet(props);
task();
props.content = "hi";
task();
调用栈
结果
观察现象
这个案例与案例一不同的地方在于,props一开始已经被定义了。
当props发生改变时,闭包内部的props发生了变化。
可以推断的是,这个闭包内部的props保存的是外部那个props的引用,所以才会有这个效果。

变形

var greet = function (props) {
  return () => console.log(props.content);
};
var props = { content: "hello" };
var task = greet(props);
task();
props = { content: "hi" };
task();
输出
观察现象
这个结果佐证了上面说的。确切来说,闭包保存的是值的引用而不是值本身
即,当我重新对外部props赋值,对闭包内所存的那个旧props引用不影响。

案例三

那么案例一可以变形为

var greet = function (props) {
  return (name) => console.log(props.content, name);
};

class Greeter {
  constructor(props) {
    this.props = props;
    this.task = greet(this.props);
  }
}

const instance = new Greeter({content: "hello"});
instance.task("jack");
instance.props.content = "hi";
instance.task("jack");
输出

小结

当入参为引用数据类型时,闭包内部维护的是传入的值的引用。

案例四

入参为基本数据类型时

var greet = function (props) {
  return (name) => console.log(props, name);
};

var props = "hello";
var task = greet(props);
task("Jack");
props = "hi";
task("Jack");
输出
作用域
观察现象
闭包内部维护了一个同名的局部变量,保存的是第一次调用时传入的,作用域为函数内部。

小结

概念

闭包是什么?
首先,要有闭包肯定得有函数。

函数与作用域链

作用域链

作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象

普通函数

function compare(value1, value2) {
  if (value1 < value2) {
    return - 1;
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}

var result = compare(1, 2);
普通函数的作用域

观察现象

作用域图例
来源:JavaScript高级程序设计

(以下序号与图中相匹)

  1. 创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[scope]]属性中。

    创建时[[scope]]
  2. 调用compare()函数时,会为函数创造一个执行环境,然后通过复制函数的[[scope]]属性中的对象构建起执行环境的作用域链。
    也就是说,全局变量对象是在执行函数时复制了创建函数时就已经确定好的那些变量对象。

    调用时compare作用域
  3. 此后又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。

闭包 Closure

function createComparisonFunction(propertyName) {
  return function compare(object1, object2) {
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];
    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}

var compareNames = createComparisonFunction("name"); // 创建函数
var result = compareNames({ name: "jack" }, { name: "tony" }); // 调用函数
compareNames = null;  // 释放内存

作用域图例

来源:JavaScript高级程序设计

步骤

  1. 创建compare时


    创建时
  2. 创建compare后

  1. 执行compare时
    createComparisonFunction的活动对象propertyName被存在了compare [[scope]]属性的Closure里。
    函数执行的时候,从Closure内访问propertyName
    执行中
    作用域

变形1

把他改成文章最开始的那个例子,如果Closure内的变量是引用类型会怎样

function createComparisonFunction(referObj) {
  return function compare(object1, object2) {
    var value1 = object1[referObj.value];
    var value2 = object2[referObj.value];
    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}
var referObj = { value: "name" };
var compareNames = createComparisonFunction(referObj); // 创建函数
referObj.value = "gender";
var result = compareNames(
  { name: "jack", gender: "man" },
  { name: "tony", gender: "woman" }
); // 调用函数
compareNames = null;
修改传入的referObj后
可以看到修改传入的referObj后,闭包所持有的那个referObj也发生了变化,印证了小结里的结论。

变形2

如果不是修改而是直接变更referObj的值

结果
可以看到Closure持有的referObj仍然是最开始的那一个

总结

对于闭包来说:

创建函数时

函数的[[scope]]属性拥有两个引用:一个是Global、一个是Closure

创建函数时

函数执行时

作用域链如下


作用域链

闭包内所存活动对象

值类型存值,引用类型存引用。

上一篇 下一篇

猜你喜欢

热点阅读