兄台: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
复制代码



相关文章
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
23天前
|
JavaScript 前端开发
js 闭包的优点和缺点
【10月更文挑战第27天】JavaScript闭包是一把双刃剑,在合理使用的情况下,它可以带来很多好处,如实现数据封装、记忆功能和模块化等;但如果不注意其缺点,如内存泄漏、变量共享和性能开销等问题,可能会导致代码出现难以调试的错误和性能问题。因此,在使用闭包时,需要谨慎权衡其优缺点,根据具体的应用场景合理地运用闭包。
106 58
|
23天前
|
缓存 JavaScript 前端开发
js 闭包
【10月更文挑战第27天】JavaScript闭包是一种强大的特性,它可以用于实现数据隐藏、记忆和缓存等功能,但在使用时也需要注意内存泄漏和变量共享等问题,以确保代码的质量和性能。
36 7
|
25天前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包:解锁编程潜能,释放你的创造力
【10月更文挑战第25天】本文深入探讨了JavaScript中的闭包,包括其基本概念、创建方法和实践应用。闭包允许函数访问其定义时的作用域链,常用于数据封装、函数柯里化和模块化编程。文章还提供了闭包的最佳实践,帮助读者更好地理解和使用这一强大特性。
15 2
|
1月前
|
设计模式 JavaScript 前端开发
探索JavaScript中的闭包:从基础概念到实际应用
在本文中,我们将深入探讨JavaScript中的一个重要概念——闭包。闭包是一种强大的编程工具,它允许函数记住并访问其所在作用域的变量,即使该函数在其作用域之外被调用。通过详细解析闭包的定义、创建方法以及实际应用场景,本文旨在帮助读者不仅理解闭包的理论概念,还能在实际开发中灵活运用这一技巧。
|
1月前
|
缓存 JavaScript 前端开发
深入了解JavaScript的闭包:概念与应用
【10月更文挑战第8天】深入了解JavaScript的闭包:概念与应用
|
1月前
|
自然语言处理 JavaScript 前端开发
Javascript中的闭包encloure
【10月更文挑战第1天】闭包是 JavaScript 中一种重要的概念,指函数能够访问其定义时的作用域内的变量,即使该函数在其词法作用域之外执行。闭包由函数及其词法环境组成。作用域链和词法作用域是闭包的核心原理。闭包常用于数据隐藏和封装,如模块模式;在异步操作中也广泛应用,如定时器和事件处理。然而,闭包也可能导致内存泄漏和变量共享问题,需谨慎使用。
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理、应用与代码演示
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理、应用与代码演示
|
2月前
|
JSON JavaScript 前端开发
JavaScript第五天(函数,this,严格模式,高阶函数,闭包,递归,正则,ES6)高级
JavaScript第五天(函数,this,严格模式,高阶函数,闭包,递归,正则,ES6)高级
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript闭包:原理与应用
【10月更文挑战第11天】深入理解JavaScript闭包:原理与应用
20 0