学习过程:
promise—>箭头函数—>作用域—>闭包
js基础
入门篇
基本语法
变量提升
JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。其中,函数名同样被视为变量名,所以一样会被提升到代码头部。
标识符
- 不能以数字开头,可以以 _ 和 $ 开头
- JavaScript 有一些保留字,不能用作标识符:arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield。
标签
js允许语句前面可以带有标签,方便跳出块区域和多重循环,常与for循环、continue、break连用。
数据类型
##null,布尔值
boolean
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true。
- undefined
- null
- false
- 0
- NaN
- “”或’’(空字符串)
注意,{}
和[]
都是true。
数值
- js中所有数都是小数,64位,其中符号位1位,表示指数的11位,剩余的52位表示精度,但是由于表示精度的最高位1.xxxx中的1被省略,所以可以表示53位精度;
- parseInt即将字符串转换为整数型,不管是不是字符串,都会先转换为字符串,再转为整数型,其中,parseInt可以传入两个参数,第二个参数可以是转换进制值;
字符串
想输出多行字符串,有一种利用多行注释的变通方法
1
2
3
4
5
6
7
8(function () { /*
line 1
line 2
line 3
*/}).toString().split('\n').slice(1, -1).join('\n')
// "line 1
// line 2
// line 3"
Base64转码
- Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+和/这64个字符组成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。
JavaScript 原生提供两个 Base64 相关的方法。
- btoa():任意值转为 Base64 编码
- atob():Base64 编码转为原来的值
1
2
3var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"
* 要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。
1
2
3
4
5
6
7
8
9
10
function b64Encode(str) {
return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
对象
- 对象的引用
两个变量同时指向同一个对象,其中任何一个变量添加属性,另一个变量都可以读写该属性,但是如果取消某个变量对于原对象的引用,并不会影响到另一个变量;
注意:仅限于对象,如果两个变量同时指向同一个原始类型的值,那么变量此时都是值的拷贝而已; 如果行首是一个大括号,js引擎一律将其解释为代码块,如果想要表示为对象,则必须在大括号前加上一个圆括号,这种差异在eval语句(作用是对字符串求值)中体现的尤为明显;
1
2eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}属性读取:一种使用点运算符,另外一种采用方括号运算符,注意若使用方括号运算符,键名必须放在引号里面,否则会被当做变量处理:
1
2
3
4
5
6
7
8
9var foo = 'bar';
var obj = {
foo: 1,
bar: 2
};
obj.foo // 1
obj[foo] // 2
查看一个对象本身所有属性,可以使用Object.keys方法
1
2
3
4
5
6
7var obj = {
key1: 1,
key2: 2
};
Object.keys(obj);
// ['key1', 'key2']属性的遍历:for…in 循环:
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
它不仅遍历对象自身的属性,还遍历继承的属性。
1
2
3
4
5
6
7
8
9
10
11
12var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
console.log('键名:', i);
console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3
with语句:作用是操作同一个对象的多个属性时,提供方便,但是不建议使用,因为在with语句里面没有改变作用域,导致绑定对象不明确,如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。
1
2
3
4
5
6
7
8var obj = {};
with (obj) {
p1 = 4;
p2 = 5;
}
obj.p1 // undefined
p1 // 4
函数
函数声明
function命令
1
2
3function print(s){
console.log(s);
}
函数表达式(如果function后带函数名,函数名也只在函数体内部有效)
1
var print = function(){console.log(s)};
Function构造函数
1
2
3
4
5
6
7
8
9
10var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
函数ToString
可以解析注释实现多行字符串:
1 | var multiline = function (fn) { |
函数作用域
在ES6中,有三种作用域:全局作用域、块作用域、函数作用域。
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
1 | var a = 1; |
上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2。
总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
所以说,如果函数A调用函数B,函数B不会引用函数A的内容。
参数传值
- 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递;
- 函数参数如果是复合类型的值(数组、对象、其他函数),传递方式是传址传递;
函数内部修改的如果是参数对象的某个属性,那么会影响到原始值,如果是替换掉整个参数,则不会影响到原始值:
1
2
3
4
5
6
7
8var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
arguments
- arguments对象包含了函数运行时的所有参数,但是它并不是一个数组,而是一个对象,所以它不能用数组中的slice和forEach方法;
- 在正常模式下,arguments对象可以在运行时修改,但是在严格模式下,修改arguments对象的值并不会改变真实参数的值;
如果要让arguments对象使用数组方法,真正的解决办法就是让arguments对象变为数组,常用的转换方法有两种,分别是
slice
方法和逐一填入新数组:1
2
3
4
5
6
7var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
闭包
闭包,简而言之就是函数中的函数,闭包最大的特点,就是它可以“记住”诞生的环境,用处有三个:
1. 可以读取函数内部的变量;
2. 让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在;
3. 封装对象的私有属性和私有方法。
1 | function createIncrementor(start) { |
1 | function Person(name) { |
“立即调用的函数表达式”(Immediately-Invoked Function Expression)
1 | (function(){ /* code */ }()); |
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
eval
- 本质是在当前域中注入代码,可以接受一个字符串作为参数,并且将该字符串当做语句执行,如果识别不是字符串,则会原样返回。
- 同时,eval没有自己的作用域,会改变当前域原有变量的值,当然在严格模式下,eval内部自己声明的变量有自己的作用域,不会影响到外部作用域的值。
- 由于引擎无法分辨eval的别名调用,所以在eval使用别名时,一律都是全局作用域。
数组
数组遍历
- 数组遍历可以考虑使用for循环、while循环、forEach循环,会返回键值(键名为整数),for…in…循环会同时遍历非整数键(返回的是键名);
- 数组遍历空位时,需要格外注意,空位和undefined是不一样的,空位表示数组中没有这个元素,所以在for…in…和forEach循环中不会被遍历到,但是undefined代表数组有这个元素,是会遍历到的;
运算符
算术运算符
加法运算符
- 对象的相加
如果运算子是对象,必须先转成原始类型的值,引擎会自动调用obj.valueof().toString(),所以我们可以重新valueOf()和toString()方法即可,其中有个特例,当运算子是一个Date对象的实例,那么会优先执行toString方法。 - 指数运算符
**
为指数运算符,是右结合的,当然,三元条件运算符也是右结合的,当然还有赋值运算符(=)也是右结合的。
比较运算符
- 严格相等运算符
JavaScript 提供两种相等运算符:== 和 === 。
简单说,它们的区别是相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将它们转换成同一个类型,再用严格相等运算符进行比较。
tip: undefined和null与自身严格相等布尔运算符
略二进制位运算符
略
其他运算符,运算顺序
void 运算符
void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined,主要用途是浏览器的书签工具,以及在超链接中插入代码防止网页跳转。
语法专题
错误处理机制
原生错误类型
- SyntaxError
- ReferenceError
- RangeError
- TypeError
- URIError
- EvalError
自定义错误
除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象
1 | function UserError(message) { |
上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义类型的错误了
1 | new UserError('这是自定义的错误!'); |
标准库
Object
四个用法:
- Object本身就是一个函数,可以将任意值转换为对象;
- 不仅可以当做工具函数使用,还可以当做构造函数使用,即前面可以加new命令;
1 | 注意,通过var obj = new Object()的写法生成新对象,与字面量的写法var obj = {}是等价的。或者说,后者只是前者的一种简便写法。 |
Object的静态方法,例如Object.print,指部署在Object对象自身的方法;
Object的实例方法,即定义在Object.prototype对象上的方法。
- Object.prototype.valueof()
- Object.prototype.toString()
- Object.prototype.toLocaleString()
- Object.prototype.hasOwnProperty()
- Object.prototype.isPrototypeof()
- Object.prototype.propertyIsEnumerable()
属性描述对象
js提供了一个内部数据结构,用来描述对象的属性,比如是否可写可读可遍历,这个内部数据结构就称之为“属性描述对象”。
- tip:Object.getOwnPropertyNames方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。而这跟Object.keys的行为不同,Object.keys只返回对象自身的可遍历属性的全部属性名。
1 | Object.keys([]) // [] |
上面代码中,数组自身的length属性是不可遍历的,Object.keys不会返回该属性。第二个例子的Object.prototype也是一个对象,所有实例对象都会继承它,它自身的属性都是不可遍历的。
- Object.defineProperty(),Object.defineProperties()
Object.defineProperty()方法接收三个参数,依次如下:- Object:属性所在的对象
- propertyName:字符串,表示属性名
- attributesObject:属性描述对象
1 | var obj = Object.defineProperty({}, 'p', { |
Object.defineProperties()接收两个参数,如下:
1 | var obj = Object.defineProperties({}, { |
- Object.prototype.propertyIsEnumerable()
返回一个布尔值,用来判断自身属性是否可以遍历。Object.keys是返回所有可以遍历的属性,不包括继承的属性,而Object.getOwnPropertyNames则是返回包括继承的属性在内的所有可以遍历的属性。
es6入门
len和const命令
len
len没有变量提升
必须先声明后才能使用,注意len变量只在块区域内有用,是块级作用域,而var不同,var具有全局作用域、函数作用域;
暂时性死区
1 | var tmp = 123; |
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
非常隐蔽的“死区”:
1 | function bar(x = y, y = 2) { |
顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。但在ES6中,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
1 | var a = 1; |