Object-Based
) 的语言First-class Function
)根据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 内部,会为函数对象添加了两个隐藏属性
属性的值就是函数名称。
function test(){
let name = '789';
console.log(name);
}
如果某个函数没有设置函数名, 该函数对象的默认的 name 属性值就是 ""
。表示该函数对象没有被设置名称。
(function (){
var test = 1
console.log(test)
})()
code值表示「函数代码」,以字符串的形式存储在内存中。
当执行到,一个「函数调用」语句时,V8 便会从函数对象中取出 code 属性值(也就是函数代码),然后再解释执行这段函数代码。
在解释执行函数代码的时候,又会生成该函数对应的执行上下文,并被推入到调用栈里。
我们通过Chrome_devTool中的工具来验证刚才的论证。(我是用Chromium:95版本)
最后不要忘记点击Enter
执行代码。
function Parent(){
}
let c1 = new Parent();
c1.fn = function fn_name_789(){
console.log('789')
}
c1.fn2 = function(){
console.log('匿名函数')
}
将开发者工具切换到 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
根据词法作用域的规则,内部函数 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时,在进行「变量查找」时候,根据作用域链来查找。
这里再多说一句:
❝每个闭包都有三个作用域: 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