兄台:JS闭包了解一下

简介: 函数即对象闭包


唯一让人恐惧的就是恐惧本身

简明扼要

  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 中每个对象就是由一组组属性和值构成的集合

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

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

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

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

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

函数的本质

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

函数是一种特殊的对象

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

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属性

属性的值就是函数名称。

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

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

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

code属性

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

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

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

验证

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

Sources新增Snippets

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

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

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. 把内部函数引用的外部变量保存到

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

// 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
复制代码



相关文章
|
21天前
|
自然语言处理 JavaScript 前端开发
JavaScript中闭包:概念、用途与潜在问题
【4月更文挑战第22天】JavaScript中的闭包是函数及其相关词法环境的组合,允许访问外部作用域,常用于数据封装、回调函数和装饰器。然而,不恰当使用可能导致内存泄漏和性能下降。为避免问题,需及时解除引用,减少不必要的闭包,以及优化闭包使用。理解并慎用闭包是关键。
|
21天前
|
JavaScript
闭包(js的问题)
闭包(js的问题)
13 0
|
21天前
|
JavaScript 前端开发
解释JavaScript闭包的工作原理,并举例说明其在游戏开发中的应用。
JavaScript闭包允许内部函数访问并保持对外部函数变量的引用,即使外部函数执行结束。当函数返回内部函数时,形成闭包,继承父函数作用域链。在游戏开发中,闭包用于创建具有独立状态和行为的角色实例。例如,`createCharacter`函数创建角色并返回包含属性和方法的对象,内部函数如`getHealth`、`setHealth`和`attack`通过闭包访问并操作角色的变量。这种方式确保了每个角色的状态在不同实例间独立,是实现游戏逻辑的强大工具。
17 2
|
21天前
|
存储 缓存 JavaScript
|
20天前
|
JavaScript 前端开发
JavaScript 闭包:让你更深入了解函数和作用域
JavaScript 闭包:让你更深入了解函数和作用域
|
20天前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包基础
JavaScript闭包基础
|
21天前
|
缓存 自然语言处理 JavaScript
JavaScript内存泄漏导致应用性能下降,常见于闭包使用不当
【5月更文挑战第14天】JavaScript内存泄漏导致应用性能下降,常见于闭包使用不当。闭包能记住并访问词法作用域,若函数返回后,其引用的对象未被释放,就会引发泄漏。例如,`createLeakyFunction`创建的闭包保留了对大型对象`someLargeObject`的引用,即使函数执行完毕,对象也无法被垃圾回收。避免泄漏的方法包括及时解除引用、清除事件监听器、使用WeakMap和WeakSet以及定期清理缓存。使用性能分析工具可检测和修复内存泄漏问题。
22 3
|
21天前
|
JavaScript 前端开发
JavaScript闭包允许内部函数访问并保留外部函数的变量,即使外部函数执行结束
【5月更文挑战第13天】JavaScript闭包允许内部函数访问并保留外部函数的变量,即使外部函数执行结束。在游戏开发中,闭包常用于创建独立状态的角色实例。例如,`createCharacter`函数生成角色,内部函数(如`getHealth`、`setHealth`)形成闭包,保存角色的属性(如生命值)。这样,每个角色实例都有自己的变量副本,不互相影响,从而实现角色系统的独立性。
23 0
|
21天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包机制
闭包是JavaScript中一个重要且常被误解的概念。本文将深入探讨闭包的本质、工作原理以及在实际开发中的应用。通过详细解析闭包的定义、作用域链、内存管理等方面,读者将对闭包有更清晰的理解,并能够运用闭包解决实际开发中的问题。
|
21天前
|
前端开发 JavaScript
闭包在JavaScript中有许多应用场景
【5月更文挑战第7天】闭包在JavaScript中发挥关键作用,如封装私有变量和函数提升安全性,维护变量生命周期,实现高阶函数,模拟块级作用域,支持回调函数以处理异步操作,以及促进模块化编程,增强代码组织和管理。闭包是理解和掌握JavaScript高级特性的重要一环。
30 7