摘要:极客时间《图解 Google V8》JavaScript 设计思想篇笔记,函数即对象、快属性和慢属性、函数表达式、原型链、作用域链、类型转换
函数即对象
JavaScript 中的函数被称为一等公民 (First Class Function),它是一种特殊的对象。
JavaScript 的对象就是由一组组属性和值组成的集合。JavaScript 是一门基于对象 (Object-Based) 的语言,但却不是一门面向对象的语言(Object—Oriented Programming Language)。面向对象语言天生支持封装、继承、多态,而 JavaScript 对多态支持不好。JavaScript 的继承也是基于原型链的继承,只是在对象中添加了一个称为原型的属性,把继承的对象通过原型链接起来。
对象的属性值有三种类型:
1、原始类型 (primitive),值本身无法被改变的数据,包括 null、undefined、boolean、number、string、bigint、symbol
2、对象类型 (Object),键值对
3、函数类型 (Function),如果对象的属性是函数,这个属性被称为方法
函数和对象一样可以拥有属性和值,不同的是函数还可以被调用。在 V8 内部,会为函数对象添加了两个隐藏属性:name 属性和 code 属性。隐藏 name 属性的值就是函数名称,name 属性值的默认值是 anonymous,表示该函数对象没有被设置名称。隐藏 code 属性则是以字符串的形式存储在内存中的函数代码,V8 调用函数时便会取出其 code 值,即函数代码,然后再解释执行这段函数代码。
如果某个编程语言的函数,可以和这个语言的数据类型做一样的事情,即可以作为函数参数,可以作为函数返回值,也可以赋值给变量,我们就把这个语言中的函数称为一等公民。
《Programming Language Pragmatics》中一等公民的定义:
In general, a value in a programming language is said to have first-class status if it can be passed as a parameter, returned from a subroutine, or assigned into a variable.
常规属性和排序属性
非线性数据结构的查询效率会低于线性的数据结构,V8 为了提升存储和查找效率,在实现对象存储时,并没有完全采用字典的存储方式,而是采用了一套复杂的存储策略。对象中的数字属性(索引属性)称为排序属性,在 V8 中被称为 elements,字符串属性(命名属性)就被称为常规属性,在 V8 中被称为 properties。ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。V8 分别使用了两个线性数据结构来分别保存排序属性和常规属性,在执行索引操作时,V8 会先从 elements 属性中按照顺序读取所有的元素,然后再在 properties 属性中读取所有的元素。
除了 elements 和 properties,对象还有__proto__
和map
属性。
快属性和慢属性
将对象不同的属性分别保存到 elements 属性和 properties 属性中,这样是简化了程序的复杂度,但是访问属性时却多了一步访问 elements 或 properties 的操作。因此,V8 为了提高查询的效率,直接将部分常规属性直接存储到对象本身,这些属性被称为对象内属性 (in-object properties)。对象内属性的数量默认是 10 个,超出的属性仍会保存在常规属性中。
保存在线性数据结构中的属性被称为快属性,因为访问速度快,但是增添和删除的开销很大。如果对象属性过多,V8 会采用“慢属性”策略:慢属性的对象内部会有独立的非线性数据结构 (词典) 作为属性存储容器,所有的属性元信息也是直接保存在属性字典中。
在 Chrome 开发者工具的 Memory 标签,点击左侧的小圆圈就可以捕获当前的内存快照,进而分析当前对象的存储信息。
函数表达式
定义函数有两种方法:
1、函数声明,语法function 函数名称 (参数:可选){ 函数体 }
。V8 会在编译阶段将函数声明转换为内存中的函数对象,并将其放到作用域中。
2、函数表达式,语法function 函数名称(可选)(参数:可选){ 函数体 }
。V8 在编译阶段不会执行表达式,在执行阶段才会执行。
通过语法来看,如果不声明函数名称,它肯定是表达式,可如果声明了函数名称的话,需要通过上下文来区分:如果它是作为赋值表达式的一部分,它就是一个函数表达式;如果是被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。被括号括住的肯定是表达式,因为括号()
是一个分组操作符,它的内部只能包含表达式。
函数表达式与函数声明的区别:
1、函数表达式在表达式语句中使用 function
2、可以省略函数表达式中函数名称,这样会创建匿名函数(anonymous functions)
3、一个函数表达式可以被用作一个即时调用的函数表达式——IIFE(Immediately Invoked Function Expression)。
立即调用的函数表达式(IIFE)
被括号括住的肯定是表达式,表达式会返回一个函数对象,如果直接在表达式后面加上调用的括号,就被称为立即调用函数表达式(IIFE)。如
1 | (function () { |
使用立即调用函数表达式的好处就是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到。
原型链
不同的语言实现继承的方式是不同的,其中最典型的两种方式是基于类的设计和基于原型继承的设计。JavaScript 是基于原型的继承,class 关键字是一个语法糖。
JavaScript 的每个对象都包含了一个隐藏属性__proto__
,该隐藏属性被称为该对象的原型 (prototype),__proto__
指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。也就是说,当试图获取一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的__proto__
(也就是它的构造函数的显式原型 prototype)中寻找,直到找到或找不到。这条由__proto__
组成的查找链被称为原型链。查看原型及原型链
继承
继承就是一个对象可以访问另外一个对象中的属性和方法,JavaScript 通过原型和原型链的方式来实现了继承特性。隐藏属性__proto__
可以实现继承,但不能在项目中通过__proto__
来访问或者修改该属性,因为它是隐藏属性,并不是标准定义的,而且使用该属性会造成严重的性能问题,一般使用构造函数来实现继承。查看继承
作用域链
作用域就是用来存放变量和函数的地方,全局作用域中存放了全局环境中声明的变量和函数,函数作用域中存放了函数中声明的变量和函数。作用域链就是将一个个作用域串起来,实现变量查找的路径。讨论作用域链,实际就是在讨论按照什么路径查找变量的问题。全局作用域是在 V8 启动过程中就创建了,且一直保存在内存中不会被销毁的,直至 V8 退出。函数作用域是在执行该函数时创建的,当函数执行结束之后,函数作用域就随之被销毁掉了。查看《浏览器工作原理与实践》笔记(二)
JavaScript 是基于词法作用域的,词法作用域是静态作用域,它根据函数在代码中的位置来确定的,作用域在声明函数时就确定好了。动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用,即动态作用域的作用域链是基于调用栈的,而不是基于函数定义的位置的。
类型转换
在计算机科学中,类型系统(type system)用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,如何操作这些类型,这些类型如何互相作用。编译器或者解释器会根据类型来限制一些有害的或者没有意义的操作,每个语言的类型系统存在差异,所以当处理同样的表达式时,返回的结果是不同的。类型是高级语言中的概念,对机器语言来说,所有的数据都是一堆二进制代码。CPU 处理这些数据的时候,并没有类型的概念,CPU 所做的仅仅是移动数据。
在 JavaScript 中,类型系统是依据 ECMAScript 标准来实现的,所以 V8 会严格根据 ECMAScript 标准来执行。查看包装对象、Number、Boolean、StringObject对象