
一、JavaScript基础
简介
- 1995年由Netscape提出,从简单的输入验证器发展成强大的变成语言。随着微软的JScipt加入竞争,形成了ECMAScipt的标准。JS版本号不一,一般都以ECMAScript兼容性和对DOM支持情况为准。
- JavaScript是一种轻量级的脚本语言(script language),同时也是一种嵌入式语言(embedded language)。JavaScript的宿主环境有多种,最常见的环境就是浏览器,还有服务器环境(Node项目)。从语法角度看,JavaScript语言是一种“对象模型”语言,但并不是纯粹的“面向对象语言”。JavaScript语言本身,虽然是一种解释型语言,但是在现代浏览器中,JavaScript都是编译后运行。JavaScript是一种开放的语言。它的标准ECMA-262是ISO国际标准,该标准的主要实现(比如V8和SpiderMonkey引擎)都是开放的,不存在版权和专利的问题。
- JavaScript组成:ECMAScript,是核心,包含语言的语法(类型、语句、关键字、保留字、操作符、对象)。浏览器宿主下提供DOM(文档对象模型)、BOM(浏览器对象模型,如navigator、location、screen对象、cookies支持等)。
- 服务器宿主下提供操作系统的API。JavaScript 程序可以采用事件驱动(event-driven)和非阻塞式(non-blocking)设计,适合服务器端高并发环境。
- 在HTML中,通过script标签元素来使用JS,且标签尽量放到/body标签前来加快解析。如果放到head元素中,需要等到所有js代码被下载、解析和执行完成才能呈现内容。当然也可使用defer属性,将外部脚本延迟到整个页面都解析完毕后再运行。可使用async属性告诉浏览器立即下载外部脚本。
- 通过noscript标签平稳退化不支持JS的浏览器,使用注释掉的[CData[JSCode]]片段来平稳退化XHTML。
- JavaScript的使用越来越广泛,在浏览器的平台化、Node、数据库操作、移动平台开发、内嵌脚本语言、跨平台的桌面应用程序(Google 的Chrome App项目、GitHub的Electron项目)。
- JavaScript的语法简单,但存在复杂性。首先,它涉及大量的外部API和兼容问题。其次,它有一些设计缺陷,导致会出现怪异的运行结果,学习的很大一部分时间是用来搞清楚哪些地方有陷阱。
语法
- 动态类型,变量可以随时改变类型
- 定义变量用var操作符,重新声明一个已经存在的变量,是无效的。如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值。
- undefined:变量未赋值,null:变量类型为Object的空值(空指针)
- 变量名区分大小写
- 标识符(变量、函数、参数、属性等),驼峰
- 单行多行注释
- 语句分号结尾
数据类型
有6种,其中number、string、boolean属于基本类型,undefined、null可以看成两个特殊值,也可以归类为基本类型。object属于引用类型,又可分为三个子类型,object(狭义对象)、array(数组)、function(函数)。
- typeof操作符,返回多一个function。
- undefined(未定义)派生自null值(空值),相等性测试返回true。null转为数字自动变为0,undefined转为数字为NaN。
- undefined、null、0/NaN(非数字)、空字符串,转换成布尔为false。空数组和空对象,转换成布尔为true。
- JavaScript内部,所有数字都是以64位(1位符号位+11位指数部分+52位小数部分)浮点数形式储存,即使整数也是如此,浮点计算会产生舍入误差。所以1和1.0是相同的,是同一个数。和java的double类型基本一致。精度只能到2^53的整数,数值范围2^-1023到2^1024之间Number.MIN_VALUE和Number.MAX_VALUE为ECMAScript能保存的最大和最小值,超出了会转换成Infinity。isNaN用来判断是否是“非数值”。
- 0b代表二进制,0o代表八进制、0x代表十六进制,十进制没有前导0。
- 数值转换,Number()用于任何类型、parseInt(string,进制)和parseFloat()用于字符串转换,进制默认为10进制。
- string,可使用单引号(推荐)或双引号表示,有些特殊字符字面量需要使用’'转义。可使用 num.toString(进制)将其他类型转换为字符串,进制默认为10进制。字符串可视为字符数组,但无法改变单个字符的值。
- JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。
- object,定义:var o =new Object();
操作符
1、一元操作符
- 加减乘除运算符 x+y, x-y, x*y, x/y
- 指数运算符:x**y
- 余数运算符:x%y
- 自增自减运算符: x++, x–, ++x, –x
2、位操作符,涉及补码,负数的补码=正数的二进制的反码加1。
- 按位非 NOT(~)
- 按位与 AND(&)
- 按位或 OR(|)
- 按位异或 XOR(^)
- 左移(<<),不影响符号位
- 有符号的右移(>>),不影响符号位
- 无符号的右移(>>>),如果是负数,会变成正数
3、布尔操作符
- 逻辑非(!)
- 逻辑与(&&)
- 逻辑或(||)
- 三元条件运算符(?:)
4、相等操作符
- 相等/不相等(==/!=),转换后比较
- 全等/不全等(===/!==),仅比较不转换
5、其他运算符
- void运算符,执行一个表达式返回undefined,常用于插入代码防止页面跳转
- 逗号运算符,先执行逗号之前的操作,然后返回逗号后面的值。
6、运算顺序
- 运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)
- 圆括号可以用来提高运算的优先级
- 大多数运算符是“左结合”,少数运算符是“右结合”,其中最主要的是赋值运算符(=),指数运算符(**)和三元条件运算符(?:)。
流控制语句
- if
- do-while
- while
- for
- for-in
三、引用类型
在ECMAScript中,引用类型是一种数据结构,可以认为是类(两者概念不一致)。对象是某个特定引用类型的实例。新对象使用new操作符后跟一个构造函数来创建。
1、Object类型
1 | var person=new Object(); |
2、Array类型
Array可保存任何类型的数据,大小可动态调整。
1 | // 1、创建数组 |
3、函数类型
- 函数是一段可以反复调用的代码块。定义函数时指定形参,调用函数时提供实参。
- JavaScript将函数看作一种值,与其它值地位相同。凡是可以使用值的地方,就能使用函数,所以称函数为第一等公民。
- 函数名的提升,当函数调用在函数声明之前时,JS将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。如果使用赋值语句声明函数,还是会报错。
- 构造函数,是创建对象的函数。首字母大写。
- 赋给一个对象属性的函数成为方法。方法可以使用this来引用调用这个方法的对象。
1 | // 1、函数的声明和调用 |
4、函数参数传递
两种不同数据类型的值
1、基本类型值,指简单的数据段,对于undefined、null、boolean、number、string这5种简单数据类型可以直接操作保存在变量中的实际值,也就是按值访问。
2、引用类型值,指那些可能由多个值构成的对象,只能操作对象的引用而不是实际的对象,所以要得到引用类型这种值只能按引用访问。
声明变量时不同的内存分配
1、基本类型值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。
这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 — 栈中,这样存储便于迅速查寻变量的值。
2、引用类型值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。
这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

3、不同的内存分配机制也带来了不同的访问机制:
在ECMAScript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是按引用访问。而基本类型的值则是可以直接访问到的。
复制变量时的不同
1、基本类型值:在将一个保存着基本类型值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
2、引用类型值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量(因为这个引用值只能按引用访问得到),也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。
这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了。
函数参数的传递
首先明确一点:ECMAScript中所有函数的参数都是按值传递的。基本类型,按值传递。数组或对象时,按引用对象指针的值传递,也称作按共享传递。
但是为什么涉及到基本数据类型与引用类型的值时仍然有区别呢,还不就是因为内存分配时的差别。
1、基本类型值:只是把变量里的值传递给参数,之后参数和这个变量互不影响,这个很好理解,不再赘述。
2、引用类型值:对象变量里面的值是这个对象在堆内存中的内存地址,因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。

用代码解释可能理解更形象。
1 | // 参数的传递 |
总结
在ECMAScript中,所谓参数按值传递就是,将形式参数初始化为实际参数的值。或者说将所传递的实际参数的值复制到函数内部的arguments数组中。对于基本类型值,就是在函数内部创建了一个该值的副本;对于引用类型值则复制了一个指向某个对象的指针。所传递的参数依次对应于arguments数组中的arguments[0],arguments[1]等元素。
而按引用传递,则相当于将形式参数初始化为实际参数本身。
注意:一个是只传递变量的值,一个是传递整个变量。这两者的内涵是完全不同的。
我们知道,ECMAScript中,每个变量都是一个用于保存值的占位符。如果参数是对象,则参数按值传递,意味着在函数内部,只能操作实际参数(对象)的属性和值,而不能操作实际参数(对象)本身。也就是说,你可以通过函数来改变函数外部所传入对象(参数)的属性和属性值,但你不能删除该变量,或改变此变量的引用对象。
所以在对象参数传递时,最重要的是要理解,所谓的按值传递,意味着函数只能操作对象的属性和值,而不能操作对象本身。
准确的说,JS中的基本类型按值传递,对象类型按共享传递的(call by sharing,也叫按对象传递、按对象共享传递)。最早由Barbara Liskov. 在1974年的GLU语言中提出。该求值策略被用于Python、Java、Ruby、JS等多种语言。
该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。如上面例子中,不可以通过修改形参o的值,来修改实参的值。虽然引用是副本,引用的对象是相同的。它们共享相同的对象,所以修改形参对象的属性值,也会影响到实参的属性值。
5、执行环境和作用域
- 变量的作用域在编程技能中算是一个基本概念,而在Javascript中,这一基本概念往往挑战者初学者的常识。Javascript这门语言与其他的大部分语言相比,有很多特殊性,这是很多人喜欢它或者讨厌它的原因。其中变量的作用域问题,对很多初学者来说就是一个又一个「坑」。
- 作用域(scope)指的是变量存在的范围。在ES5的规范中,JavaScript只有两种作用域:一种是全局作用域,在函数外声明,变量在整个程序中一直存在,所有地方都可以读取。另一种是函数作用域,变量只在函数内部存在,为局部变量,函数结束就消失。ES6新增的块级作用域会在es6单独介绍。
- 声明的变量会提升,声明的函数会提升,函数内部声明的变量同样会提升。
几个概念
1、执行环境(execution context),定义了变量或函数有权访问的其他数据,决定了它们各自的行为,执行环境有全局执行环境和函数执行环境之分。对应chrome中的Scope,分为Local和Global。
2、变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中,比如全局执行环境中是window对象。如果该环境是函数,变量对象只有在函数活动期间存在,就是活动对象,活动对象最初只包含一个变量,即 arguments 对象。每个执行环境都有一个与之关联的变量对象。
3、某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出,如关闭网页或浏览器,才被销毁)。也就是进行自动垃圾回收。
4、作用域链(scope chain),当执行流进入一个函数时,函数环境就会被推入一个环境栈,在函数执行之后,栈将其环境弹出,把控权返回给之前的执行环境。作用域链就是环境栈。每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链。对应chrome中的Call Stack。
理解作用域链
有一段代码,放在全局作用域中
1 | function func1() { |
这段代码的作用域可以这样理解,每个内部环境可以通过作用域链依次访问它外部环境,但外部环境不能访问内部环境中的任何变量和函数。
- 开始时,作用域链(环境栈)只有 Global(Window)
- 当执行 func1(),作用域链(环境栈)推入 func1,变成:func1, Global。查找任何变量,先在 func1 的执行环境中找,如果找不到,再在 Global 的执行环境中找。
- 当执行到 func2(),作用域链(环境栈)推入 func2,变成:func2, func1, Global
- func2 执行完毕后,func2 的环境被销毁,内存释放,作用域链(环境栈)变为:func1, Global
- func1 执行完毕,作用域链(环境栈)变为:Global
- 再执行 func3,作用域链(环境栈)变为:func3, Global
- func3 执行完毕,作用域链(环境栈)变为:Global
- 当浏览器(或标签)关闭,Global 销毁。
理解变量的作用域
1 | var scope = 'global'; |
上面的例子中,声明了全局变量 scope 和函数体内的局部变量 scope。在函数体内部,局部变量的优先级比通明的全局变量要高,如果一个局部变量的名字与一个全局变量相同,那么,在声明局部变量的函数体范围内,局部变量将覆盖同名的全局变量。
下面再看一个例子:
1 | scope = 'global'; |
对于初学者来说,可能会有两个疑问:为什么在函数体外,scope 的值也变成了 local ?为什么在函数体外可以访问 myScope 变量?
这两个问题都源于一个特性。在全局作用域中声明变量可以省略 var 关键字,但是如果在函数体内声明变量时不使用 var 关键字,就会发生上面的现象。首先,函数体内的第一行语句,把全局变量中的 scope 变量的值改变了。而在声明 myScope 变量时,由于没有使用 var 关键字,Javascript 就会在全局范围内声明这个变量。因此,在声明局部变量时使用 var 关键字是个很好的习惯。
ES5没有「块级作用域」
1 | if (true) { |
这里是在一个if语句中定义了变量color。如果是在C、C++或Java中,color会在if语句执行完毕后被销毁。但在JavaScript中,if语句中的变量声明会将变量添加到当前的执行环境(在这里是全局环境)中。
如果要更加强调上文中 函数中声明的所有变量,无论是在哪里声明的,在整个函数中它们都是有定义的 这句话,那么还可以在后面跟一句话:函数中声明的所有变量,无论是在哪里声明的,在整个函数中它们都是有定义的,即使是在声明之前。对于这句话,有个经典的困扰初学者的「坑」。
1 | var a = 2; |
上面的例子中,控制台输出变量 a 的值为 undefined,既不是全局变量 a 的值 2,也不是局部变量 a 的值 10。首先,局部变量在整个函数体内都是有定义的,因此,局部变量 a 会在函数体内覆盖全局变量 a,而在函数体内,在 var 语句之前,它是不会被初始化的。如果要读取一个未被初始化的变量,将会得到一个默认值 undefined。
所以,上面示例中的代码与下面的代码时等价的:
1 | var a = 2; |
可见,把所有的函数声明集合起来放在函数的开头是个良好的习惯。
函数本身的作用域
1 | // 函数本身的作用域。函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。 |
闭包(closure)
闭包是 JavaScript 语言的一个难点,也是它的特色。
1 | //JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。 |
JS变量的本质
可能很多人已经注意到,在 Javascript 当中,一个变量与一个对象的一个属性,有很多相似的地方,实际上,它们并没有什么本质区别。在 Javascript 中,任何变量都是某个特定对象的属性。
全局变量都是全局对象的属性。在 Javascript 解释器开始运行且没有执行 Javascript 代码之前,会有一个「全局对象」被创建,然后 Javascript 解释器会给它与定义一些属性,这些属性就是我们在 Javascript 代码中可以直接使用的内置的变量和方法。之后,每当我们定义一个全局变量,实际上是给全局对象定义了一个属性。
在客户端的 Javascript 当中,这个全局变量就是 Window 对象,它有一个指向自己的属性 window ,这就是我们常用的全局变量。
对于函数的局部变量,则是在函数开始执行时,会有一个对应的「调用对象」被创建,函数的局部变量都作为它的属性而存储。这样可以防止局部变量覆盖全局变量。
- window对象、document对象
1.javascript-algorithms(基于javascript的算法和数据结构)
https://github.com/trekhleb/javascript-algorithms
2.nodebestpractices(Node.js最佳实践)
https://github.com/goldbergyoni/nodebestpractices
3.You-Dont-Know-JS(你不知道的js这本书的开源版本)
https://github.com/getify/You-Dont-Know-JS
4.clean-code-javascript(教你如何写出更好可读性的js代码)
https://github.com/ryanmcdermott/clean-code-javascript
5.30-seconds-of-code(开发时常用的简短代码)
https://github.com/30-seconds/30-seconds-of-code
注:github后面加1s进入类似vscode的样式可以更好的阅读代码