You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

200 lines
12 KiB
Markdown

2 years ago
# 02 | 函数即对象一篇文章彻底搞懂JavaScript的函数特点
你好,我是李兵。这是我们专栏的第二讲,我们来看下“函数是一等公民”背后的含义。
如果你熟悉了一门其他流行语言再来使用JavaScript那么JavaScript中的函数可能会给你造成一些误解比如在JavaScript中你可以将一个函数赋值给一个变量还可以将函数作为一个参数传递给另外一个函数甚至可以使得一个函数返回另外一个函数这在一些主流语言中都很难实现。
JavaScript中的函数非常灵活其根本原因在于**JavaScript中的函数就是一种特殊的对象**我们把JavaScript中的函数称为**一等公民(First Class Function)。**
基于函数是一等公民的设计使得JavaScript非常容易实现一些特性比如闭包还有函数式编程等而其他语言要实现这些特性就显得比较困难比如要在C++中实现闭包需要实现大量复杂的代码,而且使用起来也异常复杂。
函数式编程和闭包在实际的项目中会经常遇到,如果不了解这些特性,那么在你使用第三方代码时就会非常吃力,同时自己也很难使用这些特性写出优雅的代码,因此我们很有必要了解这些特性的底层机制。
另外在我们后续课程介绍V8工作机制时会学习V8是怎么实现闭包的还会学习V8是如何将JavaScript的动态特性静态化以加快代码的执行速度这些内容都涉及到JavaScript中的函数底层特性。
今天我们就来深入分析下JavaScript中的“函数”到底有怎样的特点。
## 什么是JavaScript中的对象
既然在JavaScript中函数就是一种特殊的对象那我们首先要明白什么是JavaScript中的“对象”它和面向对象语言中的“对象”有什么区别
和其他主流语言不一样的是JavaScript是一门**基于对象(****Object-Based)**的语言可以说JavaScript中大部分的内容都是由对象构成的诸如函数、数组也可以说JavaScript是建立在对象之上的语言。
![](https://static001.geekbang.org/resource/image/9e/f8/9e946bbdc54f5e1347f7b593f8f6fff8.jpg "基于对象的设计")
而这些对象在运行时可以动态修改其内容这造就了JavaScript的超级灵活特性。不过因为JavaScript太灵活了也加大了理解和使用这门语言的难度。
虽然JavaScript是基于对象设计的但是它却不是一门**面向对象的语言(Object—Oriented Programming Language)**,因为面向对象语言天生支持**封装、继承、多态**但是JavaScript并没有直接提供多态的支持因此要在JavaScript中使用多态并不是一件容易的事。
![](https://static001.geekbang.org/resource/image/ef/00/eff1c1c773835b79ce597a84b2f94a00.jpg "面向对象的语言")
除了对多态支持的不好JavaScript实现继承的方式和面向对象的语言实现继承的方式同样存在很大的差异。
面向对象语言是由语言本身对继承做了充分的支持并提供了大量的关键字如public、protected、friend、interface等众多的关键字使得面向对象语言的继承变得异常繁琐和复杂而JavaScript中实现继承的方式却非常简单清爽**只是在对象中添加了一个称为原型的属性,把继承的对象通过原型链接起来,就实现了继承,我们把这种继承方式称为基于原型链继承。**关于V8是如何支持原型的我们会在《[05 | 原型链V8是如何实现对象继承的](https://time.geekbang.org/column/article/215425)》这节课做具体介绍。
既然“JavaScript中的对象”和“面向对象语言中的对象”存在巨大差异那么在JavaScript中我们所谈论的对象到底是指什么呢
其实JavaScript中的对象非常简单每个对象就是由一组组属性和值构成的集合比如我使用下面代码创建了一个person对象
```
var person=new Object();
person.firstname="John";
person.lastname="Doe";
person.age=50;
person.eyecolor="blue";
```
这个对象里面有四个属性,为了直观理解,你可以参看下图:
![](https://static001.geekbang.org/resource/image/d0/23/d07e174001a29765a3575908e3704123.jpg "对象的构成")
上图展示了对象person的结构我们可以看到蓝色的属性在左边黄色的值在右边有多组属性和值组成这就是JavaScript中的对象虽然JavaScript对象用途非常广泛使用的方式也非常之多但是万变不离其宗其核心本质都就是由一组组属性和值组成的集合抓住了这一点当我们再分析对象时就会轻松很多。
之所以JavaScript中对象的用途这么广是因为对象的值可以是任意类型的数据我们可以改造下上面的那段代码来看看对象的值都有哪些类型改造后的代码如下所示
```
var person=new Object()
person.firstname="John"
person.lastname="Doe"
person.info = new Object()
person.info.age=50
person.info.eyecolor="blue"
person.showinfo = function (){
console.log(/*...*/)
}
```
我们可以先画出这段代码的内存布局,如下图所示:
![](https://static001.geekbang.org/resource/image/f7/17/f73524e4cae884747ae528d999fc1117.jpg "属性值类型")
观察上图,我们可以看出来,对象的属性值有三种类型:
第一种是**原始类型(primitive)**所谓的原始类的数据是指值本身无法被改变比如JavaScript中的字符串就是原始类型如果你修改了JavaScript中字符串的值那么V8会返回给你一个新的字符串原始字符串并没有被改变我们称这些类型的值为“原始值”。
JavaScript中的原始值主要包括null、undefined、boolean、number、string、bigint、symbol 这七种。
第二种就是我们现在介绍的**对象类型(Object)**对象的属性值也可以是另外一个对象比如上图中的info属性值就是一个对象。
第三种是**函数类型(Function)**如果对象中的属性值是函数那么我们把这个属性称为方法所以我们又说对象具备属性和方法那么上图中的showinfo就是person对象的一个方法。
![](https://static001.geekbang.org/resource/image/8c/a6/8c33fa6c6e0cef5795292f0a21ee36a6.jpg "对象属性值的三种类型")
## 函数的本质
分析完对象现在我们就能更好地理解JavaScript中函数的概念了。
在这节课开始我就提到在JavaScript中函数是一种特殊的对象它和对象一样可以拥有属性和值但是函数和普通对象不同的是函数可以被调用。
我们先来看一段JavaScript代码在这段代码中我们定义了一个函数foo接下来我们给foo函数设置了myName和uName的属性。
```
function foo(){
var test = 1
}
foo.myName = 1
foo.uName = 2
console.log(foo.myName)
```
既然是函数,那么它也可以被调用。比如你定义了一个函数,便可以通过函数名称加小括号来实现函数的调用,代码如下所示:
```
function foo(){
var test = 1
console.log(test)
}
foo()
```
除了使用函数名称来实现函数的调用,还可以直接调用一个匿名函数,代码如下所示:
```
(function (){
var test = 1
console.log(test)
})()
```
那么V8内部是怎么实现函数可调用特性的呢
其实在V8内部会为函数对象添加了两个隐藏属性具体属性如下图所示
![](https://static001.geekbang.org/resource/image/9e/e2/9e274227d637ce8abc4a098587613de2.jpg "函数对象具有隐藏属性")
也就是说函数除了可以拥有常用类型的属性值之外还拥有两个隐藏属性分别是name属性和code属性。
隐藏name属性的值就是函数名称如果某个函数没有设置函数名如下面这段函数
```
(function (){
var test = 1
console.log(test)
})()
```
该函数对象的默认的name属性值就是anonymous表示该函数对象没有被设置名称。另外一个隐藏属性是code属性其值表示函数代码以字符串的形式存储在内存中。当执行到一个函数调用语句时V8便会从函数对象中取出code属性值也就是函数代码然后再解释执行这段函数代码。
## 函数是一等公民
因为函数是一种特殊的对象所以在JavaScript中函数可以赋值给一个变量也可以作为函数的参数还可以作为函数的返回值。**如果某个编程语言的函数,可以和这个语言的数据类型做一样的事情,我们就把这个语言中的函数称为一等公民。**支持函数是一等公民的语言可以使得代码逻辑更加清晰,代码更加简洁。
但是由于函数的“可被调用”的特性,使得实现函数的可赋值、可传参和可作为返回值等特性变得有一点麻烦。为什么?
我们知道在执行JavaScript函数的过程中为了实现变量的查找V8会为其维护一个作用域链如果函数中使用了某个变量但是在函数内部又没有定义该变量那么函数就会沿着作用域链去外部的作用域中查找该变量具体流程如下图所示
![](https://static001.geekbang.org/resource/image/8b/fd/8bb90b190362e3a00e5a260bad6829fd.jpg "查找变量")
从图中可以看出,当函数内部引用了外部的变量时,使用这个函数进行赋值、传参或作为返回值,你还需要保证这些被引用的外部变量是确定存在的,这就是让函数作为一等公民麻烦的地方,因为虚拟机还需要处理函数引用的外部变量。我们来看一段简单的代码:
```
function foo(){
var number = 1
function bar(){
number++
console.log(number)
}
return bar
}
var mybar = foo()
mybar()
```
观察上段代码可以看到我们在foo函数中定义了一个新的bar函数并且bar函数引用了foo函数中的变量number当调用foo函数的时候它会返回bar函数。
那么所谓的“函数是一等公民”就体现在如果要返回函数bar给外部那么即便foo函数执行结束了其内部定义的number变量也不能被销毁因为bar函数依然引用了该变量。
我们也把这种将外部变量和和函数绑定起来的技术称为闭包。V8在实现闭包的特性时也做了大量的额外的工作关于闭包的详细实现我们会在《[12 | 延迟解析V8是如何实现闭包的](https://time.geekbang.org/column/article/223168)》这节课再介绍。
另外基于函数是一等公民我们可以轻松使用JavaScript来实现目前比较流行的函数式编程函数式编程规则很少非常优美不过这并不是本专栏的重点所以我们先略开不讲。
## 总结
好了,今天的内容就介绍到这里,下面我来总结下本文的主要内容。
本文我们围绕JavaScript中的函数来展开介绍JavaScript中的函数非常灵活既可以被调用还可以作为变量、参数和返回值这些特性使得函数的用法非常多这也导致了函数变得有些复杂因此本文的目的就是要讲清楚函数到底是什么
因为函数是一种特殊的对象所以我们先介绍了JavaScript中的对象JavaScript中的对象就是由一组一组属性和值组成的集合既然函数也是对象那么函数也是由一组组值和属性组成的集合我们还在文中使用了一段代码证明了这点。
因为函数作为一个对象,是可以被赋值、作为参数,还可以作为返回值的,那么如果一个函数返回了另外一个函数,那么就应该返回该函数所有相关的内容。
接下来,我们又介绍了一个函数到底关联了哪些内容:
* 函数作为一个对象,它有自己的属性和值,所以函数关联了基础的属性和值;
* 函数之所以成为特殊的对象,这个特殊的地方是函数可以“被调用”,所以一个函数被调用时,它还需要关联相关的执行上下文。
结合以上两点JavaScript中的函数就实现了“函数是一等公民”的特性。
## 思考题
本文我们从对象聊到了闭包,那么留给你的问题是,哪些语言天生支持“函数是一等公民”?欢迎你在留言区与我分享讨论。
感谢你的阅读,如果你觉得这一讲的内容对你有所启发,也欢迎把它分享给你的朋友。