写在前面的话
JavaScript 语言的内容,前面基本上也记录的差不多了。这里就聊一些JavaScript语言更深入的问题,加深对这个语言的理解。C 和 Java 始终是 JavaScript 的基础,很多概念都是直接继承过来的,所以学习 C 是很重要的。我基本上每年都会对 C 有一个回顾,然后把数据结构的书再看一遍。
扯远了,这边只是记录 JavaScript 一些知识点,让我以后更好地上手 JavaScript,也是学习React的一个必要的过程。虽然我已经有一个上线的 React 项目,但是React的很多原理我基本上是抓瞎的。
作为一个移动端,在现在大前端的趋势下,多一个React的能力也挺好的。好几年前已经用Vue上线过一个项目了,但是那个项目比较简单,所以几年过去,我基本上忘的差不多了。
继承与原型链
对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个 class 实现。(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。
尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。
基于原型链的继承
JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
遵循ECMAScript标准,someObject.[[
Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问。这个等同于JavaScript的非标准但许多浏览器实现的属性 proto。但它不应该与构造函数
func的prototype属性相混淆。被构造函数创建的实例对象的 [[Prototype]] 指向 func 的prototype属性。Object.prototype属性表示Object的原型对象。
1 | // 让我们从一个函数里创建一个对象o,它自身拥有属性a和b的: |
在 JavaScript 中使用原型
接下去,来仔细分析一下这些应用场景下, JavaScript 在背后做了哪些事情。
正如之前提到的,在 JavaScript 中,函数(function)是允许拥有属性的。所有的函数会有一个特别的属性 —— prototype 。请注意,以下的代码是独立的(出于严谨,假定页面没有其他的JavaScript代码)。为了最佳的学习体验,我们强烈建议阁下打开浏览器的控制台(在Chrome和火狐浏览器中,按Ctrl+Shift+I即可),进入“console”选项卡,然后把如下的JavaScript代码复制粘贴到窗口中,最后通过按下回车键运行代码。
1 | function doSomething(){} |
在控制台显示的JavaScript代码块中,我们可以看到doSomething函数的一个默认属性prototype。而这段代码运行之后,控制台应该显示类似如下的结果:
1 | { |
现在我们可以通过new操作符来创建基于这个原型对象的doSomething实例。使用new操作符,只需在调用doSomething函数语句之前添加new。这样,便可以获得这个函数的一个实例对象。一些属性就可以添加到该原型对象中。
1 | function doSomething(){} |
如上所示, doSomeInstancing 中的__proto__是 doSomething.prototype. 但这是做什么的呢?当你访问doSomeInstancing 中的一个属性,浏览器首先会查看doSomeInstancing 中是否存在这个属性。
如果 doSomeInstancing 不包含属性信息, 那么浏览器会在 doSomeInstancing 的 __proto__ 中进行查找(同 doSomething.prototype). 如属性在 doSomeInstancing 的 __proto__ 中查找到,则使用 doSomeInstancing 中 __proto__ 的属性。
否则,如果 doSomeInstancing 中 __proto__ 不具有该属性,则检查doSomeInstancing 的 __proto__ 的 __proto__ 是否具有该属性。默认情况下,任何函数的原型属性 __proto__ 都是 window.Object.prototype. 因此, 通过doSomeInstancing 的 __proto__ 的 __proto__ ( 同 doSomething.prototype 的 __proto__ (同 Object.prototype)) 来查找要搜索的属性。
如果属性不存在 doSomeInstancing 的 __proto__ 的 __proto__ 中, 那么就会在 doSomeInstancing 的 __proto__ 的 __proto__ 的 __proto__ 中查找。然而, 这里存在个问题:doSomeInstancing 的 __proto__ 的 __proto__ 的 __proto__ 其实不存在。因此,只有这样,在 __proto__ 的整个原型链被查看之后,这里没有更多的 __proto__ , 浏览器断言该属性不存在,并给出属性值为 undefined 的结论。
使用不同的方法来创建对象和生成原型链
使用语法结构创建的对象
1 | var o = {a: 1}; |
使用构造器创建的对象
1 | function Graph() { |
使用 Object.create 创建的对象
ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:
1 | var a = {a: 1}; |
使用 class 关键字创建的对象
ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 class, constructor,static,extends 和 super。
1 | ; |
内存管理
像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。相反,JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。
内存生命周期
不管什么程序语言,内存生命周期基本是一致的:
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放\归还
所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。
JavaScript 的内存分配
为了不让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。
1 | var n = 123; // 给数值变量分配内存 |
通过函数调用分配内存
1 | var d = new Date(); // 分配一个 Date 对象 |
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
当内存不再需要使用时释放
大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。
垃圾回收
如上文所述自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。
这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
1 | var o = { |
该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
1 | function f(){ |
这里的内存管理讲的很一般,如果要详细的了解,还是要去看 C 的指针部分的内容。不过 C 的指针内容很复杂,需要慢慢斟酌,慢慢理解
函数
函数算是 js 里面花样最多的了,其他语言也有闭包,函数式编程,但是花样这么多,用法这么乱的挺少的。起码Swift的 函数真的很好用,然后对于引用对象的 拷贝也是正常的 C 的逻辑,js的我现在很难理解,也看不到底层的内存分布是怎么个逻辑。
还是先看看 函数的 教程吧
箭头函数
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
语法
基础语法
1 | (param1, param2, …, paramN) => { statements } |
高级语法
1 | //加括号的函数体返回对象字面量表达式: |
描述
引入箭头函数有两个方面的作用:更简短的函数并且不绑定this。
更短的函数
1 | var elements = [ |
没有单独的this
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值:
- 如果是该函数是一个构造函数,this指针指向一个新的对象
- 在严格模式下的函数调用下,this指向undefined
- 如果是该函数是一个对象的方法,则它的this指针指向这个对象
- 等等
This被证明是令人厌烦的面向对象风格的编程。
1 | function Person() { |
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。因此,在下面的代码中,传递给setInterval的函数内的this与封闭函数中的this值相同.
严格模式的其他规则依然不变.
1 | function Person(){ |
通过 call 或 apply 调用
由于 箭头函数没有自己的this指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定this—译者注),他们的第一个参数会被忽略。(这种现象对于bind方法同样成立—译者注)
1 | var adder = { |
箭头函数不绑定Arguments 对象。因此,在本示例中,arguments只是引用了封闭作用域内的arguments:
1 | var arguments = [1, 2, 3]; |
最后
就到这里吧,js的内容看得我头疼,又很困。