❝
唯一让人恐惧的就是恐惧本身
❞
简明扼要
- JS是一门基于对象 (
Object-Based) 的语言 - 对象是由数据、方法以及关联原型三个组成部分
- 函数是一种特殊的对象
- 函数是一等公民(
First-class Function) - 根据「词法作用域」的规则,内部函数引用外部函数的变量被保存到内存中,而这些变量的集合被称为闭包
- 闭包和词法环境的强相关
- 闭包在每次创建函数时创建(闭包在JS编译阶段被创建)
- 产生闭包的核心两步: 1. 预扫描内部函数 2. 把内部函数引用的外部变量保存到堆中
- 每个闭包都有三个作用域:\
- Local Scope (Own scope)\
- Outer Functions Scope\
- Global Scope
文章概要
- 函数即对象
- 闭包
函数即对象
根据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 内部,会为函数对象添加了两个隐藏属性
- name 属性
- 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):函数可以和其他的数据类型做一样的事情\
- 被当作参数传递给其他函数\
- 可以作为另一个函数的返回值\
- 可以被赋值给一个变量
❞
闭包
❝
在 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
根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 test 中的变量。
test 函数执行完成之后,其执行上下文从栈顶弹出了 但是由于返回的 setName 和 getName 方法中使用了 test 函数内部的变量 myName 和 age 所以这两个变量依然保存在内存中(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 函数执行结束之后,返回的 getName 和 setName 方法都引用“clourse(test)”对象。
即使 test 函数退出了,“clourse(test)”依然被其内部的 getName 和 setName 方法引用。
所以在下次调用t.setName或者t.getName时,在进行变量查找时候,根据作用域链来查找。
这里再多说一句:
❝
每个闭包都有三个作用域:\
- Local Scope (Own scope)\
- Outer Functions Scope\
- 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