JavaScript 基础
- JavaScript 运行在客户端的脚本语言,不需要编译,由js解释器(js引擎)逐行解释执行。Node.js也可以用于服务器端编程。
- JavaScript组成: ECMAScript(JavaScript语法)、DOM(文档对象模型)访问HTML文档的所有元素、BOM(浏览器对象模型)它使JavaScript有能力与浏览器进行对话
- **JavaScript的作用: **
- 表单动态校验(密码强度检测)
- 网页特效
- 服务端开发(Node.js)
- 桌面程序(Electron)、App(Cordova)、控制硬件-物联网(Ruff)、游戏开发(cocos2d-js)
1.javascript几种形式
- JS有三种书写方式, 分别为行内、内嵌和外部。
行内式
<input type="button" value="请点击我" onclick="alert('Hello World')" />
内嵌式
<script>alert('你好, JavaScript')</script>
外部式
<script src="./js1.js" ></script>
2. 基础语法
2.1 结束符
- JavaScript 程序的执行单位为行(line),也就是一行一行地执行。一般情况下,每一行就是一个语句。
- 语句以分号
;
结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。 ;
分号前面可以没有任何内容,JavaScript 引擎将其视为空语句。- 换行也标志着结束
2.2 注释
单行注释: //
多行注释: /*
......
*/
2.3 打印(输出内容)
输出到控制台: console.log()
输出到页面: document.write()
弹出: alert()
(警告框)
// 几种弹框 alert() //警告框, 没有返回值 confirm() //确认框, 返回布尔值 prompt() //输入框, 点确定:返回用户输入的内容, 点取消:返回null
2.4 获取页面中的元素作为js对象
document.getElementById() # 返回对象(通常会称作元素对象) /* 元素对象与HTML元素存在映射关系, 元素对象用来描述某个HTML元素 HTML元素的属性, 会映射成元素对象的属性 举个例子: <script> document.write('<p id="demo">段落,独占一行</p>'); p = document.getElementById('demo'); p.innerHTML='行内标签'; </script> */
2.5 变量
- Javascript 使用关键字var、let、const来定义变量
1) var 定义变量
var 变量名 = 值;
// var:关键字
// 变量名(标识符): 变量名由字母、数字、下划线和$组成且不能以*数字*开头,变量名不能使用关键字。中文是合法的标识符, 可以用作变量名。
// 声明变量而没给赋值, 默认是undefined, undefined 是一个特殊的值, 表示‘无意义’。
// 如果变量赋值的时候忘了写var 关键字, 这条语句也是有效的;
// 但是不写var 不利于表达意图, 而且容易不知不觉的创建全局变量, 所以建议使用var声明变量。
// 如果一个变量没有声明就直接使用, JavaScript 会报错, 告诉你变量未定义: ReferenceError: x is not defined。
var a, b;
// var 关键字还可以同时声明多个变量
var a = 1;
a = "js";
// JavaScript 是一种动态类型语言, 也就是说, 变量的类型没有限制,变量可以随时更改类型。
// 变量a 先被赋值了一个数值, 后又被重新赋值为一盒字符串。第二次赋值的时候因为变量a 已经存在, 所以不需要使用var 关键字。
var a;
// 如果同一个变量被声明了两次, 第二次声明是无效的。
var a = 2;
// 但是如果第二次声明的时候还进行了赋值, 则会覆盖掉前面的值。
==*var 变量提升: ** 使用var
关键字声明的变量会自动提升到函数作用域的顶部; 也就是把所有变量声明都拉到函数作用域的顶部==
标识符: 开发人员为变量属性函数参数名取的名字, 标识符不能是关键字或保留字 关键字: JS 本身已经用了的名字,不能再用这些充当变量名、方法名。 保留字: 就是预留的关键字, 还不是关键字未来可能会成为关键字, 同样不能使用它们当变量名或方法名。 关键字和保留字:
2) let 定义变量
- let 跟var 的作用差不多, 但是有着非常重要的区别。最明显的区别是, ==let 声明的范围是块作用域, 而var 声明的范围是函数作用域==。
if (true) { var name = "zy"; console.log(name); // zy } console.log(name); // zy // 换成let 定义变量 if (true) { let age = 18; console.log(age); // 18 } console.log(age); // arn_js.js:5 Uncaught ReferenceError: age is not defined // 可以看到age 变量不能再if 块外部被引用, 以为它的作用域仅限于该块内部。 // 块作用域是函数作用域的子集, 所以使用域var 的作用域限制同样也适用于let。 var name; var name; var age; let age; // ntaxError: Identifier 'age' has already been declared // let 也不允许同一个作用域中出现出现冗余声明, 会导致报错; // Javascript 引擎会记录用于变量声明的标识符及其所在的块作用域, 所以嵌套使用相同的标识符不会报错, 因为在同一个块中没有重复声明。 // 对声明冗余报错不会因混用let 和var 而受影响; 这两个关键字声明的并不是不同类型的变量, 他们只是指出变量在相关作用域如何存在。
==*let变量提升:**let 与var 的另一个区别就越是let 声明的变量不会在作用域中被提升==。
3) const定义常量
- conset 与let 基本相同, 唯一一个重要的区别是用它声明变量时必须同时初始化变量, 且尝试修改const 声明的变量会导致运行时错误。
const name = "dada";
name = "da"; // typeError: Assignment to constant variable
// const 也不允许重复声明, 声明的作用域也是块。
// const 声明的限制只适用于它指向的变量的引用;换句话说, 如果const 变量引用的是一个对象, 那么修改这个对象内部的属性并不违反const的限制。
2.6 区块
- Javascript 使用大括号, 将多个相关的语句组合在一起, 称为“区块”(block)。
- 对于 var 关键字来说, Javascript 的区块不构成单独的作用域(scope)。
2.7 条件语句
- Javascript 提供
if
结构和switch
结构, 完成条件判断, 即只有满足预设的条件才会执行相应的语句。
1) if 结构
if
结构先判断一个表达式的布尔值, 然后根据布尔值的真伪, 执行不同的语句。布尔值: true、false
2) if...else 结构
if
代码块后面,还可以跟一个else
代码块, 表示不满足条件时, 所要执行的代码。
3) switch 结构
- 多个
if...else
连在一起使用的时候, 可以转为更方便的switch
结构。
4) 三元运算符[ ?: ]
- Javascript 还有一个三元运算符(即该运算符需要三个运算子)
?:
, 也可以用于逻辑判断。(条件) ? 表达式1 : 表达式2
2.8 循环语句
- 循环语句用于重复执行某个操作, 它有多种形式。
1) while 循环
while
语句包括一个循环条件和一段代码块, 只要条件为真, 就不断循环执行代码块。
2) for 循环
for
语句是循环命令的另一种形式, 可以指定循环的起点、终点和终止条件。
3) do...while 循环
do...while
循环与while
循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件。
4) break、continue 语句
-
break
语句和continue
语句都具有跳转作用,可以让代码不按既有的顺序执行。 -
break
语句用于跳出代码块或循环。var i = 0; while(i < 100) { console.log('i 当前为:' + i); i++; if (i === 10) break; } // 上面代码只会执行10次循环, 一旦i等于10, 就会跳出循环 // for循环也可以使用break语句跳出循环。 for (var i = 0; i < 5; i++) { console.log(i); if (i === 3) break; }
-
continue
语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环。var i = 0; while (i < 100){ i++; if (i % 2 === 0) continue; console.log('i 当前为:' + i); } // 上面代码只有在i为奇数时, 才会输出i的值。如果i为偶数, 则直接进入下一轮循环。
-
如果存在多重循环,不带参数的
break
语句和continue
语句都只针对最内层循环。
2.9 代码规范:
1) 标识符命名规范
- 变量、函数的命名必须要有意义
- 变量的名称一般用名词
- 函数的名称一般用动词
2) 单行注释规范
- // 单行注释前面注意有个空格
3) 其它规范
- 关键词 操作符空格
3. 数据类型
- JavaScript 语言的每一个值, 都属于某一种数据类型。Javascript 有以下几种数据类型:
- undefined(未定义): 表示"未定义" 或不存在, 由于没有定义, 所以此处暂时没有任何值; 默认值 "undefined"
- boolean(布尔): 表示真(
true
)和假()的两个特殊的值; 默认值 false - string(字符串): 文本; 默认值 " "
- number(数字): 整数和小数; 默认值 0
- object(对象): 各种值组成的集合
- array(数组): 数组
- function(函数): 函数
- symbol(符号): 符号类型
- null(空值): 即此处的值为空, 默认值 null】 其中Null 和undefined 都是被动产生
3.1 typeof 运算符
typeof
运算符可以返回一个值的数据类型。
3.2 null 、undefined 和 NaN
-
null
与undefined
都可以表示“没有”, 含义非常相似; 将一个变量赋值为undefined
或null
, 语法效果几乎没区别。 -
null
表示空值, 即该处的值现在为空。调用函数时, 某个参数未设置任何值, 这时就可以传入null
, 表示该参数为空。 -
if
语句中,它们都会被自动转为false
, 相等运算符(==
)甚至直接报告两者相等
3.3 Boolean 布尔值
-
布尔值往往用于程序流程的控制
// 注意, 空数组([])和空对象({})对应的布尔值, 都是true if ([]) { console.log('true'); // true } if ({}) { console.log('true'); // true }
3.4 Number 数字
1) 整数和浮点数
-
Javascript 内部所有数字都是以64为浮点数形式存储, 即使整数也是如此。
// 1 与 1.0 是相同的, 是同一个数 1 === 1.0 // true // Javascript 语言的底层根本没有整数, 所有数字都是小数(64位浮点数)。 // 容易造成混淆的是, 某些运算只有整数才能完成, 此时Javascript 会自动把64位浮点数, 转成32位整数然后再进行运算。 // 注意浮点数不是精确的值, 所以涉及小数的比较和运算要特别小心。 0.1 + 0.2 === 0.3; // false 0.3 / 0.1; // 2.9999999999999996 (0.3 - 0.2) === (0.2 - 0.1) // flase
2) 数值精度
-
根据国际标准 IEEE 754, JavaScript 浮点数的64个二进制位, 从最左边开始, 是这样组成的。
-
第1位:符号位,
0
表示正数,1
表示负数 -
第2位到第12位(共11位):指数部分
-
第13位到第64位(共52位):小数部分(即有效数字)
==符号位决定了一个数的正负, 指数部分决定了数值的大小, 小数部分决定了数值的精度。==
-
-
指数部分一共有11个二进制位, 因此大小范围就是0到2047。
-
IEEE 754 规定, 如果指数部分的值在0到2047之间(不含两个端点), 那么有效数字的第一位默认总是1, 不保存在64位浮点数之中。
-
也就是说, 有效数字这时总是
1.xx...xx
的形式,其中xx..xx
的部分保存在64位浮点数之中, 最长可能为52位。因此, JavaScript 提供的有效数字最长为53个二进制位。
3) 数值范围
-
64位浮点数的指数部分的长度是11个二进制位, 意味着指数部分的最大值是2047(2的11次方减1)。
-
也就是说, 64位浮点数的指数部分的值最大为2047, 分出一半表示负数, 则 JavaScript 能够表示的数值范围为21024到2-1023(开区间), 超出这个范围的数无法表。
// 如果一个数大于等于2的1024次方, 那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数, 这时就会返回`Infinity` Math.pow(2, 1024); // Infinity // 如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位), 那么就会发生为“负向溢出”, 即Javascript 无法表示这么小的数, 这时会直接返回0。 Math.pow(2, -1076); // 0 // 例子: var x = 0.5; for(var i = 0; i < 25; i++) { x = x * x; // 0.5连续做25次平方, 由于最后结果太接近0, 超出了可表示的范围Javascript 就直接将其转为 0。 }
-
Javascript 提供
Number
对象的MAX_VALUE
和MIN_VALUE
属性, 返回可以表示的具体的最大值和最小值。Number.MAX_VALUE // 1.7976931348623157e+308 Number.MIN_VALUE // 5e-324
5) 数值表示法
-
数值也可以采用科学计数法表示, 科学计数法允许字母
e
或E
的后面跟着一个整数, 表示这个数值的指数部分。
6) 数值的进制
-
使用字面量直接表示一个数值时, Javascript 对整数提供四种进制的表示方法: 十进制、二进制、八进制和十六进制。
- 十进制:没有前缀
0
的数值。 - 八进制:有前缀
0o
或0O
的数值, 或者有前导0、且只用到0-7的八个阿拉伯数字的数值。 - 十六进制:有前缀
0x
或0X
的数值。 - 二进制:有前缀
0b
或0B
的数值。
// Javascript 内部会自动将八进制、十六进制、二进制转为十进制。 0xff // 255 0o377 // 255 0b11 // 3 // 如果八进制、十六进制、二进制的数值里面, 出现不属于该进制的数字就会报错 0xzz // 报错 0o88 // 报错 0b22 // 报错 // 十六进制出现了字母z、八进制出现数字8、二进制出现数字2, 因此报错。 // 有前导0的数值会被视为八进制, 但是如果前导0后面有数字8和9, 则该数值被视为十进制。 0888 // 888 0777 // 511 // 处理时很容易造成混乱。ES5 的严格模式和ES6已经废除了这种表示法, 但是浏览器为了兼容以前的代码, 目前还继续支持这种表示法。
- 十进制:没有前缀
7) 正零和负零
-
Javascript 的64位浮点数之中, 有一个二进制位是符号位。这意味着, 任何一个数都有一个对应的负值, 就连
0
也不例外。// Javascript 内部实际上存在2个0:一个是+0, 一个是-0, 区别就是64位浮点数表示法的符号位不同。它们是等价的。 -0 === +0 // true 0 === -0 // true 0 === +0 // true // 几乎所有场合, 正零和负零都会被当作正常的0 +0 // 0 -0 // 0 (-0).toString() // '0' (+0).toString() // '0' // 唯一有区别的场合是, +0或-0当作分母, 返回的值是不相等的。 (1 / +0) === (1 / -0) // 结果是false 是因为除以正零得到+Infinity, 除以负零得到-Infinity, 这两者是不相等的。
8) Infinity 无穷
-
Infinity
表示“无穷”, 用来表示两种场景。一种是一个正的数值太大或一个负的数值太小无法表示;另一种是非0数值除以0得到Infinity
。// 场景1 Math.pow(2, 1024) // Infinity // 场景2 0 / 0 // NaN 1 / 0 // Infinity 0除以0会得到NaN,而非0数值除以0,会返回Infinity。
-
Infinity
有正负之分,Infinity
表示正的无穷,-Infinity
表示负的无穷。Infinity === -Infinity // false 1 / -0 // -Infinity -1 / -0 // Infinity // 非零正数除以-0,会得到-Infinity, 负数除以-0,会得到Infinity
-
由于数值正向溢出(overflow)、负向溢出(underflow)和被
0
除, JavaScript 都不报错, 所以单纯的数学运算几乎没有可能抛出错误。// Infinity大于一切数值(除了NaN), -Infinity小于一切数值(除了NaN) Infinity > 1000 // true -Infinity < -1000 // true // Infinity与NaN比较, 总是返回false。 Infinity > NaN // false -Infinity > NaN // false Infinity < NaN // false -Infinity < NaN // false
-
Infinity 运算
Infinity
的四则运算, 符合无穷的数学计算规则- 0乘以
Infinity
返回NaN
;0除以Infinity
返回0
;Infinity
除以0返回Infinity
Infinity
加上或乘以Infinity
返回的还是Infinity
。Infinity
减去或除以Infinity
得到NaN
。Infinity
与null
计算时null
会转成0,等同于与0
的计算。Infinity
与undefined
计算, 返回的都是NaN
。
9) 数值相关的全局方法
-
parseInt()
方法用于将字符串转为整数。 -
parseFloat()
方法用于将一个字符串转为浮点数。 -
isNaN()
方法可以用来判断一个值是否为NaN
isFinite(-1) // true
3.5 String 字符串
-
字符串就是零个或多个排在一起的字符, 放在单引号
'
或双引号"
之中。 -
单引号字符串的内部可以使用双引号。双引号字符串的内部可以使用单引号。
-
如果要在单引号字符串的内部使用单引号,就必须在内部的单引号前面加上反斜杠
\
用来转义,双引号字符串内部使用双引号也是如此。 -
字符串默认只能写在一行内, 分成多行将会报错; 如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。
1) 转义
-
反斜杠
\
在字符串内有特殊含义用来表示一些特殊字符, 所以又称为转义符 -
主要的反斜杠转义字符主要有以下这些:
转义符号 含义 unicode编码 \0
null \u0000 \b
后退键 \u0008 \f
换页符 \u000C \n
换行符 \u000A \r
回车键 \u000D \t
制表符 \u0009 \v
垂直制表符 \u000B \'
单引号 \u0027 \"
双引号 \u0022 \\
反斜杠 \u005C
2) 字符串与数组
-
字符串可以被视为字符数组, 可以使用数组的方括号
[]
运算符用来返回某个位置的字符(位置索引从0开始) -
如果方括号中的数字超过字符串的长度, 或者方括号中根本不是数字, 则返回
undefined
。
3) length 长度
-
length
属性返回字符串的长度,该属性也是无法改变的。
4) 字符集
-
Javascript 使用
Unicode
字符集。JavaScript 引擎内部所有字符都用 Unicode 表示。 -
Javascript 不仅以 Unicode 储存字符, 还允许直接在程序中使用 Unicode 码点表示字符, 即将字符写成
\uxxxx
的形式, 其中xxxx
代表该字符的 Unicode 码点。
5) Base64 转码
-
有时文本里面包含一些不可打印的符号, 比如ASCII 码0到31的符号都无法打印出来, 这时可以使用Base64 编码, 将它们转成可以打印的字符;
另一个场景是, 有时需要以文本格式传递二进制数据, 那么也可以使用 Base64 编码。
-
Base64 就是一种编码方法可以将任意值转成 0~9、A~Z、a-z、
+
和/
这64个字符组成的可打印字符。使用它的主要目的不是为了加密, 而是为了不出现特殊字符, 简化程序的处理。 -
JavaScript 原生提供两个Base64 相关的方法:
btoa()
:从二进制数据“字符串”创建一个 Base-64 编码的 ASCII 字符串(“btoa”应读作“binary to ASCII”)atob()
:解码通过 Base-64 编码的字符串数据(“atob”应读作“ASCII to binary”)
3.6 object 对象
-
对象(object)是 JavaScript 语言的核心概念。也是最重要的数据类型
-
什么是对象?简单说, 对象就是一组“键值对”(key-value)的集合, 是一种无序的复合数据集合。
1) 键名
-
对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名), 所以加不加引号都可以。
-
如果键名是数值, 会被自动转为字符串
-
对象的每一个键名又称为“属性”(property), 它的“键值”可以是任何数据类型; 如果一个属性的值为函数, 通常把这个属性称为“方法”, 它可以像函数那样调用。
-
如果属性的值还是一个对象, 就形成了链式引用。
-
对象的属性之间用逗号分隔, 最后一个属性后面可以加逗号, 也可以不加。
-
属性可以动态创建,不必在对象声明时就指定。
2) 对象的引用
-
如果不同的变量名指向同一个对象, 那么它们都是这个对象的引用, 也就是说指向同一个内存地址; 修改其中一个变量, 会影响到其他所有变量。
-
如果取消某一个变量对于原对象的引用, 不会影响到另一个变量
3) 属性的操作
属性的读取:
-
读取对象的属性有两种方法: 一种是使用点
.
运算符,还有一种是使用方括号[]
运算符。
属性的赋值:
-
点运算符和方括号运算符, 不仅可以用来读取值, 还可以用来赋值。
属性的查看:
属性的删除:
-
delete
命令用于删除对象的属性, 删除成功后返回 true。
属性是否存在:
-
in
运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值), 如果包含就返回true
否则返回false
; -
左边是一个字符串, 表示属性名, 右边是一个对象。
属性的遍历:
-
for...in
循环用来遍历一个对象的全部属性。 -
for...in
循环需要注意两点:- 它遍历的是对象所有可遍历(enumerable)的属性, 会跳过不可遍历的属性;
- 它不仅遍历对象自身的属性,还遍历继承的属性。
with语句:
- 它的作用是操作同一个对象的多个属性时, 提供一些书写的方便。语法:
with (对象) { 语句; }
-
注意: 如果
with
区块内部有变量的赋值操作, 必须是当前对象已经存在的属性, 否则会创造一个当前作用域的全局变量。
3.7 函数
- 在 Javascript 中, 函数是头等 (*first-class*)*对象,因为它们可以像任何其它*对象一样具有属性和方法, 它们与其他对象的区别在于函数可以被调用。
- 在 Javascript 中, 每个函数其实都是一个
Function
对象; - 如果一个函数中没有使用 return 语句, 则它默认返回
undefined
; 要想返回一个特定的值, 则函数必须使用 [return
] 语句来指定一个要返回的值。
1) 函数的声明(创建)
2) 圆括号运算符, return 语句和递归
-
调用函数时, 要使用圆括号运算符; 圆括号之中可以加入函数的参数。
-
函数可以调用自身, 这就是递归(recursion)。
3) 第一等公民
-
Javascript 语言将函数看作一种值, 与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方就能使用函数。
-
比如, 可以把函数赋值给变量和对象的属性, 也可以当作参数传入其他函数, 或者作为函数的结果返回;
-
函数只是一个可以执行的值, 此外并无特殊之处。
4) 函数名的提升
-
Javascript 引擎将函数名视同变量名, 所以采用
function
命令声明函数时, 整个函数会像变量声明一样被提升到代码头部。 -
如果采用赋值语句定义函数, Javascript 就会报错。
-
注意: 如果像下面例子那样, 采用function 命令和var 赋值语句声明同一个函数, 由于存在函数提升, 最后会采用var赋值语句的定义。
5) 函数的属性和方法
-
name
属性: 函数的name
属性返回函数的名字。 -
length
属性: 函数的length
属性返回函数预期传入的参数个数, 即函数定义之中的参数个数。 -
toString()
方法: 返回一个字符串, 内容是函数的源码。
6) 函数作用域
-
作用域(scope)指的是变量存在的范围。
-
在 ES5 的规范中, Javascript 只有两种作用域:
-
一种是全局作用域, 变量在整个程序中一直存在, 所有地方都可以读取;
-
另一种是函数作用域, 变量只在函数内部存在。ES6 又新增了块级作用域。
-
-
对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。
-
函数内部定义的变量, 会在该作用域内覆盖同名全局变量。
7) 函数内部的变量提升
-
与全局作用域一样, 函数作用域内部也会产生“变量提升”现象。var命令声明的变量不管在什么位置, 变量声明都会被提升到函数体的头部。
8) 函数本身作用域
-
函数本身也是一个值, 也有自己的作用域。它的作用域与变量一样, 就是其声明时所在的作用域, ==与其运行(或者说调用)时所在的作用域无关==。
9) 函数参数
-
函数运行的时候, 有时需要提供外部数据,不同的外部数据会得到不通的结果, 这种外部数据就叫参数。
-
函数的参数不是必须的, Javascript 允许省略参数。
-
没有办法只省略靠前的参数, 而保留靠后的参数。如果一定要省略靠前的参数, 只有显式传入
undefined
。 -
【函数传递方式】 函数参数如果是原始类型的值(数值、字符串、布尔值), 传递方式是==传值传递==。这就意味着函数体内修改参数值, 不会影响到函数外部。
-
**【函数传递方式】**函数参数如果是复合类型的值(数组、对象、其他函数), 传递方式是==传址传递==。也就是说传入函数的的是原始值的地址, 所以在函数内部修改参数将会影响到原始值。
-
如果有同名的参数, 则取最后出现的那个值。
10) 函数的闭包
- 闭包(closure)是Javascript 语言的一个难点, 也是它的特色。很多高级应用都要依靠闭包实现。
- 理解闭包, 首先必须理解变量作用域。前面提到Javascript 有两种作用域:全局作用域和函数作用域。
// 函数内部可以直接读取全局变量; 正常情况下, 函数外部无法读取函数内部声明的变量。 // 如果出于种种原因, 需要得到函数内的局部变量。正常情况下这是办不到的, 只有通过变通方法才能实现。那就是在函数的内部, 再定义一个函数。 function fun() { var parm = 1; function inner() { console.log(parm); } } // 上面代码中, 函数inner就在函数fun内部, 这时fun的所有局部变量对inner都是可见的; // 但是反过来就不行, inner内部的局部变量, 对fun就是不可见的。 // 这就是Javascript 语言特有的"链式作用域"结构(chain scope), 子对象会一级一级地向上寻找所有父对象的变量; // 所以, 父对象的所有变量, 对子对象都是可见的, 反之则不成立。 // 既然inner可以读取fun的局部变量, 那么只要把inner作为返回值, 就可以在f1外部读取它的内部变量了 function fun() { var parm = 1; function inner() { console.log(parm); } return inner; } var newFun = fun(); newFun() // 1 // 上面代码中, 函数fun的返回值就是函数inner, 由于inner可以读取fun的内部变量, 所以就可以在外部获得fun的内部变量了。 // 闭包就是函数inner, 即能够读取其他函数内部变量的函数。由于在Javascript 语言中, 只有函数内部的子函数才能读取内部变量; // 所以可以把闭包简单理解成“定义在一个函数内部的函数”。 // 闭包最大的特点, 就是它可以“记住”诞生的环境, 比如inner记住了它诞生的环境fun, 所以从inner可以得到fun的内部变量。 // 在本质上, 闭包就是将函数内部和函数外部连接起来的一座桥梁。
- 闭包的最大用处有两个: 一个是可以读取外层函数内部的变量, 另一个就是让这些变量始终保持在内存中, 即闭包可以使得它诞生环境一直存在。
11) 立即调用的函数表达式
- 根据Javascript 的语法, 圆括号
()
跟在函数名之后, 表示调用该函数。 - 有时我们需要在定义函数之后, 立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。
// function这个关键字既可以当作语句,也可以当作表达式。 // 语句 function f() {} // 表达式 var f = function f() {} // 当作表达式时, 函数可以定义后直接加圆括号调用。 var f = function f() { return 1}(); f // 1 函数定义后直接加圆括号调用没有报错。原因就是function作为表达式, 引擎就把函数定义当作一个值。这种情况下就不会报错。
-
为了避免解析的歧义, Javascript 规定, 如果function关键字出现在行首, 一律解释成语句。
因此,引擎看到行首是function关键字之后, 认为这一段都是函数的定义, 不应该以圆括号结尾, 所以就报错了。
// 函数定义后立即调用的解决方法, 就是不要让function出现在行首让引擎将其理解成一个表达式。最简单的处理, 就是将其放在一个圆括号里面。 (function(){ /* code */ }()); // 或者 (function(){ /* code */ })(); // 上面两种写法都是以圆括号开头, 引擎就会认为后面跟的是一个表达式, 而不是函数定义语句, 所以就避免了错误。 // 注意:上面两种写法最后的分号都是必须的。
- 推而广之, 任何让解释器以表达式来处理函数定义的方法, 都能产生同样的效果, 比如下面三种写法。
var i = function(){ return 10; }(); true && function(){ /* code */ }(); 0, function(){ /* code */ }(); // 甚至像这样写也是可以的。 !function () { /* code */ }(); ~function () { /* code */ }(); -function () { /* code */ }(); +function () { /* code */ }();
-
通常情况下, 只对匿名函数使用这种“立即执行的函数表达式”。
它的目的有两个:
- 一是不必为函数命名,避免了污染全局变量;
- 二是 IIFE(Immediately-Invoked Function Expression) 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
// 写法一 var tmp = newData; processData(tmp); storeData(tmp); // 写法二(比写法一更好, 完全避免了污染全局变量) (function () { var tmp = newData; processData(tmp); storeData(tmp); }());
12) eval
eval
命令接受一个字符串作为参数, 并将这个字符串当作语句执行。- 如果参数字符串无法当作语句运行, 那么就会报错。
- 放在
eval
中的字符串, 应该有独自存在的意义, 不能用来与eval
以外的命令配合使用。
eval('return;'); // SyntaxError: Illegal return statement // 因为return不能单独使用, 必须在函数中使用。
-
如果
eval
的参数不是字符串, 那么会原样返回。 -
eval
没有自己的作用域, 都在当前作用域内执行, 因此可能会修改当前作用域的变量的值, 造成安全问题。
var a = 1; eval('a = 2'); a // 2 // 为了防止这种风险Javascript 规定, 如果使用严格模式, eval内部声明的变量不会影响到外部作用域。 'use strict' var a = 1; eval('var a = 2'); console.log(a) // 1 // 不过,即使在严格模式下,eval依然可以读写当前作用域的变量 (function f() { 'use strict'; var foo = 1; eval('foo = 2'); console.log(foo); // 2 })() // 总之, eval的本质是在当前作用域之中注入代码。由于安全风险和不利于Javascript 引擎优化执行速度, 一般不推荐使用。 // 通常情况下, eval最常见的场合是解析JSON数据的字符串, 不过正确的做法应该是使用原生的JSON.parse方法。
3.8 数组
- 数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始), 整个数组用方括号表示。
// 除了在定义时赋值,数组也可以先定义后赋值。 var arr = []; arr[0] = 'a'; arr[1] = 1; console.log(arr) // ['a', 1] // 任何类型的数据, 都可以放入数组。 var arr = [ {a:1}, [1, 2, 3], function fun() { console.log('函数') } ]; arr[0]; arr[1]; arr[2]; // 如果数组的元素还是数组, 就形成了多维数组。 var a = [[1, 'a'], [2, 'b'], [3, 'c']] a[0][1] // a a[1][1] // b
- 本质上数组属于一种特殊的对象。
typeof
运算符会返回数组的类型是object
。 - 数组的特殊性体现在, 它的键名是按次序排列的一组整数(0,1,2...)。
var a = [[1, 'a'], [2, 'b'], [3, 'c']] Object.keys(a) // ['0', '1', '2'] Object.keys方法返回数组的所有键名 // 由于数组成员的键名是固定的(默认总是0、1、2...), 因此数组不用为每个元素指定键名, 而对象的每个成员都必须指定键名。 // Javascript 语言规定对象的键名一律为字符串。 // 所以, 数组的键名其实也是字符串。之所以可以用数值读取, 是因为非字符串的键名会被转为字符串。 a['1'] // [2, 'b'] a[1] // [2, 'b'] 数值键名被自动转为了字符串
- 对象有两种读取成员的方法:点结构(
object.key
)和方括号结构(object[key]
)。但是, 对于数值的键名,不能使用点结构。
var arr = [1, 2, 3]; arr.0 // SyntaxError arr.0的写法不合法, 因为单独的数值不能作为标识符(identifier)。所以数组成员只能用方括号arr[0]表示(方括号是运算符,可以接受数值)。
1) length 属性
length
属性返回数组的成员数量。
[[1, 'a'], [2, 'b'], [3, 'c']].length // 3 // Javascript 使用一个32位整数, 保存数组的元素个数。这意味着, 数组成员最多只有4294967295个(232 - 1)个,也就是说length属性的最大值就是4294967295。
length
属性是可写的。如果人为设置一个小于当前成员个数的值, 该数组的成员数量会自动减少到length
设置的值。
var a = [1, 'a', 2, 'b', 3, 'c']; a.length // 6 a.length = 2 a // [1, 'a'] // 当数组的length属性设为2(即最大的整数键只能是1)那么整数键2(值为2)之后的元素就已经不在数组中了, 被自动删除了。 // 清空数组的一个有效方法, 就是将length属性设为0。 // 如果人为设置length大于当前元素个数, 则数组的成员数量会增加到这个值, 新增的位置都是空位。 var a = [1, 'a']; a.length = 2 a[3] // undefined 读取新增的位置都会返回undefined // 如果人为设置length为不合法的值, Javascript 会报错。
- 注意:由于数组本质上是一种对象, 所以可以为数组添加属性, 但是这不影响
length
属性的值。
var a = []; a['p'] = 'abc'; a.length // 0 a[1.2] = 'abc' a.length // 0 // 上面代码将数组的键分别设为字符串和小数, 结果都不影响length属性。 // 因为length属性的值就是等于最大的数字键加1, 而这个数组没有整数键, 所以length属性保持为0。
- 如果数组的键名是添加超出范围的数值, 该键名会自动转为字符串。
var arr = []; arr[-1] = 'a'; arr[Math.pow(2, 32)] = 'b'; console.log(arr.length); // 0 console.log(arr[-1]); // a console.log(arr[4294967296]); // b // 数组arr添加了两个不合法的数字键, 结果length属性没有发生变化。这些数字键都变成了字符串键名。 // 之所以会取到值, 是因为取键值时, 数字键名会默认转为字符串。
2) in 运算符
- 检查某个键名是否存在的运算符
in
, 适用于对象, 也适用于数组。
var arr = [1, 2, 3, 'a', 'b', 'c']; 2 in arr // true '2' in arr // true 由于键名都是字符, 所以数值2会自动转成字符串。 6 in arr // false 如果数组的某个位置是空位, in运算符返回false
3) for...in 循环和数组的遍历
for...in
循环不仅可以遍历对象, 也可以遍历数组, 数组也是一种特殊对象。
// for...in 循环遍历的是数组的数字键 var arr = [1, 2, 3, 'a', 'b', 'c']; for (let i in arr) { console.log(i); } // 0 // 1 // 2 // 3 // 4 // 5 // for...in不仅会遍历数组所有的数字键,还会遍历非数字键 var arr = [1, 2, 3, 'a', 'b', 'c']; arr.length = 3; arr.foo = true for (let i in arr) { console.log(i); } // 0 // 1 // 2 // foo 非整数键foo也遍历到了, 所以不推荐for...in遍历数组
- 数组的遍历可以考虑使用
for
和while
循环。
var arr = ['a', 'b', 'c', 2, 3]; // for for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } // while var i = 0; while (i < arr.length) { console.log(arr[i]); i++; } // while 从右往左遍历 var arrLen = arr.length; while (arrLen--) { console.log(arr[arrLen]); }
forEach()
方法也可以遍历数组。
var arr = ['a', 'b', 'c', 2, 3]; arr.forEach(element => console.log(element));
4) 数组的空位
- 当数组的某个位置是空元素, 即两个逗号之间没有任何值, 我们称该数组存在空位。
- 数组的空位是可以读取的, 返回
undefined
。 - 使用
delete
命令删除一个数组成员, 会形成空位, 并且不会影响length
属性。 - 数组的某个位置是空位, 与某个位置是
undefined
是不一样的。如果是空位,使用数组的forEach
方法、for...in
结构、以及Object.keys
方法进行遍历,空位都会被跳过。
5) 类似数组的对象
- 如果一个对象的所有键名都是正整数或零, 并且有
length
属性, 那么这个对象就很像数组, 语法上称为“类似数组的对象”(array-like object)。
4 运算符
4.1 算数运算符
- Javascript 共提供10个算术运算符, 用来完成基本的算术运算。
运算符 | 描述 |
---|---|
+ | 加法运算符 |
- | 减法运算符 |
* | 乘法运算符 |
\ | 除法运算符 |
** | 指数运算符 |
% | 余数运算符 |
++x 或 x++ | 自增运算符 |
--x 或x-- | 自减运算符 |
+x | 数值运算符 |
-x | 负数值运算符 |
- 对象的相加:如果运算子是对象, 必须先转成原始类型的值, 然后再相加。
4.2 赋值运算符
- 赋值运算符(Assignment Operators)用于给变量赋值, 最常见的等号
=
赋值运算符。
4.3 比较运算符
- 比较运算符用于比较两个值的大小, 然后返回一个布尔值, 表示是否满足指定的条件。
运算符 | 描述 |
---|---|
> | 大于运算符 |
< | 小于运算符 |
>= | 大于或等于运算符 |
<= | 小于或等于运算符 |
== | 相等运算符 |
=== | 严格相等运算符 |
!= | 不相等运算符 |
!== | 严格不相等运算符 |
- 比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样。
- 对于非相等的比较, 算法是先看两个运算子是否都是字符串, 如果是字符串, 就按照字典顺序比较(实际上是比较 Unicode 码点);否则, 将两个运算子都转成数值, 再比较数值的大小。
-
相等运算符和严格相等运算符
-
Javascript 提供两种相等运算符:
==
和===
。 -
相等操作符
==
:- 在比较之前, 会进行类型转换。它会尝试将两个值转换为相同类型, 然后再进行比较。
- ==*类型转换规则==:
- 如果两个操作数的类型相同(例如两个数字、两个字符串等), 则不进行类型转换, 直接进行比较;
- 如果其中一个操作数是
null
, 另一个操作数是undefined
, 则它们会被视为相等; - 如果一个操作数是数字, 另一个操作数是字符串, 会将字符串转换为数字, 然后比较它们的数值;
- 如果一个操作数是布尔值, 另一个操作数是非布尔值(除了
null
和undefined
), 会将布尔值转换为数字(true
转换为 1,false
转换为 0), 然后进行比较; - 如果一个操作数是对象, 另一个操作数是原始值(字符串、数字、布尔值), 会首先调用对象的
valueOf
方法获取原始值, 如果返回的还是对象, 再接着调用toString
方法,然后按照上述规则进行比较; - 如果一个操作数是
NaN
, 另一个操作数也是NaN
, 它们被视为相等; undefined
和null
只有与自身比较, 或者互相比较时才会返回true
; 与其他类型的值比较时, 结果都为false
;- 在其他情况下,返回
false
。
-
-
不相等运算符和严格不相等运算符
4.4 布尔运算符(逻辑运算符)
- 布尔运算符用于将表达式转为布尔值
运算符 | 描述 |
---|---|
! | 取反运算符 |
&& | 且运算符 |
|| | 或运算符 |
?: | 三元条件运算符 |
4.5 二进制位运算符
- 二进制位运算符用于直接对二进制位进行计算,一共有7个。
下面这些位运算符只对整数起作用, 如果一个运算子不是整数, 会自动转为整数后再执行:
运算符 | 描述 | |
---|---|---|
\ | 二进制或运算符: 表示若两个二进制位都为0, 则结果为0, 否则为1。 | |
& | 二进制与运算符: 表示若两个二进制位都为1, 则结果为1, 否则为0。 | |
~ | 二进制否运算符: 表示对一个二进制位取反。 | |
^ | 异或运算符: 表示若两个二进制位不相同, 则结果为1, 否则为0。 | |
<< | 左移运算符 | |
>> | 右移运算符 | |
>>> | 头部补零的右移运算符 |
4.6 其它运算符,运算顺序
void
运算符的作用是执行一个表达式, 然后不返回任何值, 或者说返回undefined
。
运算符优先级:
5 数据类型转换
- Javascript 是一种动态类型语言, 变量没有类型限制, 可以随时赋予任意值。
- 如果运算符发现, 运算子的类型与预期不符, 就会自动转换类型。
5.1 强制类型转换
-
强制转换主要指使用
Number()
、String()
和Boolean()
三个函数, 手动将各种类型的值, 分别转换成数字、字符串或者布尔值。 -
Number() : 使用
Number()
函数, 可以将任意类型的值转化成数值。