多案例快速搞懂js闭包

前言

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

案例

案例一

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);
普通函数的作用域

观察现象

  • 在执行到compare时,持有this、value1、value2这三个局部变量。
    为什么说value1、value2是局部变量?简单说来,就是在compare执行时,首先在函数顶部声明了var value1=value1;var value2=value2;
  • 变形


    加了一个内部变量

    可以看到在local声明未赋值时,赋值过的value1、value2就出现在作用域内了

  • 作用域链除了这些个局部变量之外,还有一个this。到现在作用域链这个概念还是挺抽象的,没有办法一管窥全豹。
作用域图例
来源: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后

  • compare被返回之后,他的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。
    执行完创建之后
  • createComparisonFunction 执行完了,但他的活动对象不会被销毁。因为返回的那个compare作用域链仍然在引用这个活动对象。
    即,当createComparisonFunction返回之后,其执行环境的作用域链会被销毁,但他的活动对象仍然存在内存中。
  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

创建函数时

函数执行时

作用域链如下


作用域链

闭包内所存活动对象

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容