前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >兄台:JS闭包了解一下

兄台:JS闭包了解一下

作者头像
前端柒八九
发布2022-08-25 14:13:37
7560
发布2022-08-25 14:13:37
举报
文章被收录于专栏:柒八九技术收纳盒

简明扼要

  1. JS是一门基于对象 (Object-Based) 的语言
  2. 对象是由数据、方法以及关联原型三个组成部分
  3. 函数是一种特殊的对象
  4. 函数是一等公民(First-class Function)
  5. 根据「词法作用域」的规则,内部函数引用外部函数的变量被保存到内存中,而这些「变量的集合」被称为闭包
  6. 闭包和词法环境的「强相关」
  7. 闭包在每次创建函数时创建(闭包在JS编译阶段被创建)
  8. 产生闭包的核心两步: 1. 「预扫描」内部函数 2. 把内部函数引用的外部变量保存到堆中
  9. 每个闭包都有三个作用域: 1. Local Scope (Own scope) 2. Outer Functions Scope 3. Global Scope

文章概要

  1. 函数即对象
  2. 闭包

函数即对象

根据MDN描述JS特性的时候。提到

❝JavaScript is designed on a simple object-based paradigm JS是一门基于对象 (Object-Based) 的语言(也就是我们总说的JS是object-oriented programming [OOP]语言 ) ❞

JavaScript 中每个对象就是由一组组属性和值构成的集合。

代码语言:javascript
复制
var person=new Object();
person.firstname="John";
person.lastname="Doe";
person.age=50;
person.eyecolor="blue";

同时, 在 JS 中,对象的值可以是「任意类型」的数据。(在JS篇之数据类型那些事儿简单的介绍了下基本数据类型分类和判断数据类型的几种方式和原理,想了解具体细节,可移步指定文档)

在OOP的编程方式中,有一个心智模式需要了解

❝对象是由数据、方法以及关联原型三个组成部分 ❞

数据就是属性值为非函数类型(表示对象的数据属性),方法就是属性值为函数类型(表示对象的行为属性),而关联原型涉及到对象的继承。(这个我们后续会有相关介绍)。

函数的本质

在JS中,一切皆对象。那从语言的设计层面来讲,

❝函数是一种特殊的对象 ❞

它和对象一样可以拥有属性和值。

代码语言:javascript
复制
function foo(){
    var test = 1
    return test;
}
foo.myName = 1
foo.obj = { x: 1 }
foo.fun = function(){
  return 0;
}

根据对象的数据特性:foo 函数拥有myName / obj/fun 的属性

但是函数和普通对象不同的是,「函数可以被调用」

我们从V8内部来看看函数是如何实现可调用特性。

在 V8 内部,会为函数对象添加了两个隐藏属性

  1. name 属性
  2. code 属性

name属性

属性的值就是函数名称。

代码语言:javascript
复制
function test(){
  let name = '789';
  console.log(name);
}

如果某个函数没有设置函数名, 该函数对象的默认的 name 属性值就是 ""。表示该函数对象没有被设置名称。

代码语言:javascript
复制
(function (){
    var test = 1
    console.log(test)
})()

code属性

code值表示「函数代码」,以字符串的形式存储在内存中。

当执行到,一个「函数调用」语句时,V8 便会从函数对象中取出 code 属性值(也就是函数代码),然后再解释执行这段函数代码。

在解释执行函数代码的时候,又会生成该函数对应的执行上下文,并被推入到调用栈里。

验证

我们通过Chrome_devTool中的工具来验证刚才的论证。(我是用Chromium:95版本)

Sources新增Snippets

最后不要忘记点击Enter执行代码。

代码语言:javascript
复制
function Parent(){

}

let c1 = new Parent();

c1.fn = function fn_name_789(){
  console.log('789')
}

c1.fn2 = function(){
  console.log('匿名函数')
}

Memory查询内存快照

将开发者工具切换到 Memory 标签,然后点击左侧的小圆圈就可以捕获当前的「内存快照」

搜索Parent,在Parent的实例c1,可见存在两个方法属性(fn/fn2),处理该对象的隐藏类的map属性(后面我们会有文章介绍)还有继承相关的__proto__

fn是一个方法属性,也就是指向了函数对象。而通过上文得知,函数对象中包含可调用特性的属性。从图中可知,code表示函数代码(并且还是延迟编译的), 上文的name存放在shared对象中。

关于CPU如何执行程序的简单介绍,可以参考CPU如何执行程序

关于执行上下文的相关介绍,可以参考兄台: 作用域、执行上下文了解一下


针对JS的点,还有一点需要强调一下

❝函数是一等公民(First-class Function):函数可以和其他的数据类型做一样的事情 1. 被当作参数传递给其他函数 2. 可以作为另一个函数的返回值 3. 可以被赋值给一个变量 ❞


闭包

❝在 JS 中,根据「词法作用域」的规则,内部函数总是可以访问其外部函数中声明的变量。当通过调用一个外部函数「返回」一个内部函数后,即使该外部函数已经执行结束了。但是「内部函数引用外部函数的变量依然保存在内存中」,就把这些变量的集合称为闭包。 ❞

代码语言:javascript
复制
function test() {
    var myName = "fn_outer"
    let age = 78;
    var innerObj = {
        getName:function(){
            console.log(age);
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerObj
}
var t = test();
console.log(t.getName());//fn_outer 
t.setName("global")
console.log(t.getName())//global

根据词法作用域的规则,内部函数 getNamesetName 总是可以访问它们的外部函数 test 中的变量。

test 函数执行完成之后,其执行上下文从栈顶弹出了 但是由于返回的 setNamegetName 方法中使用了 test 函数内部的变量 myNameage所以这两个变量依然保存在内存中(Closure (test)

当执行到t.setName方法的时,调用栈如下:

利用debugger来查看对应的作用链和调用栈信息。

通过上面分析,然后参考作用域的概念和使用方式,我们可以做一个简单的结论

❝闭包和词法环境的「强相关」

我们再从V8编译JS的角度分析,执行JS代码核心流程 1. 先编译 2. 后执行。而通过分析得知,闭包和词法环境在某种程度上可以认为是强相关的。而JS的作用域由词法环境决定,并且作用域是「静态」的。

所以,我们可以得出一个结论:

❝闭包在每次创建函数时创建(闭包在JS编译阶段被创建) ❞

闭包是如何产生的?

闭包是什么,我们知道了,现在我们再从V8角度谈一下,闭包是咋产生的。

先上结论:

❝产生闭包的核心两步: 1.「预扫描」内部函数 2. 把内部函数引用的外部变量保存到堆中 ❞

代码语言:javascript
复制
function test() {
    var myName = "fn_outer"
    let age = 78;
    var innerObj = {
        getName:function(){
            console.log(age);
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerObj
}
var t = test();

我们,还是那这个例子来讲。

当 V8 执行到 test 函数时,首先会编译,并创建一个空执行上下文。在编译过程中,遇到内部函数 setName, V8还要对内部函数做一次「快速的词法扫描」(预扫描) 发现该内部函数引用了 test 函数中的 myName 变量。由于是内部函数引用了外部函数的变量,所以 V8 判断这是一个闭包。于是在堆空间创建换一个closure(test)的对象 (这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。

test 函数执行结束之后,返回的 getNamesetName 方法都引用“clourse(test)”对象。

即使 test 函数退出了,“clourse(test)”依然被其内部的 getNamesetName 方法引用。

所以在下次调用t.setName或者t.getName时,在进行「变量查找」时候,根据作用域链来查找。

这里再多说一句:

❝每个闭包都有三个作用域: 1. Local Scope (Own scope) 2. Outer Functions Scope 3. Global Scope ❞

代码语言:javascript
复制
// global scope
var e = 10;
function sum(a){
  return function(b){
    return function(c){
      // outer functions scope
      return function(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

console.log(sum(1)(2)(3)(4)); // log 20
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端柒八九 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简明扼要
  • 文章概要
  • 函数即对象
    • 函数的本质
      • name属性
      • code属性
      • 验证
      • Memory查询内存快照
  • 闭包
    • 闭包是如何产生的?
    相关产品与服务
    云开发 CLI 工具
    云开发 CLI 工具(Cloudbase CLI Devtools,CCLID)是云开发官方指定的 CLI 工具,可以帮助开发者快速构建 Serverless 应用。CLI 工具提供能力包括文件储存的管理、云函数的部署、模板项目的创建、HTTP Service、静态网站托管等,您可以专注于编码,无需在平台中切换各类配置。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档