大括号
特点:大括号(单独的大括号或者if等后的大括号)内是一个单独的作用域
注意点:在块级作用域内声明的函数,类似var,会被提升到大括号外,应避免在块级作用域内声明函数。如果确实需要,写成函数表达式。
{ let f = function () { // }; }
利用:代替立即执行函数。
扩展:立即执行函数(IIFE)常用于封装第三方库或者独立的功能模块,函数内定义的所有变量都是局部变量,避免了变量污染(命名冲突),不会污染全局空间。建议在自己写的立即执行函数前加分号,像下面这个会报错。
var c = 12 var d = c (function(){alert(1);})();
//Uncaught TypeError: c is not a function
立即函数使用建议和好处:
建议
(1)立即函数内部是可以访问外部变量的,所以很多情况下,我们并不需要传参数。
(2)通常不应该给立即执行函数传递太多的函数,一是造成该函数的使用有较强的依赖性。二是为了理解代码是如何工作的,不得不经常上下滚动源代码。
好处
(1)立即执行函数模式可以帮你封装大量的工作而不会在背后遗留任何全局变量。
(2)定义的所有变量都会成员立即执行函数的局部变量,不用担心这些临时变量会污染全局空间。
let
特点(const也有这些):
(1)只在声明所在的块级作用域内有效。
(2)无变量提升:let声明的变量一定要在声明后使用,否则报错;
(3)暂时性死区:如果区块(函数参数小括号,块级作用域,for循环)中存在let
命令声明的变量,凡是在声明之前就使用这些变量,就会报错(暂时性死区)。
(4)同一作用域不能重复声明同一变量。
注意以下场景:
(1)let声明的变量“绑定”(binding)区域,不再受外部的影响。
var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; }
(2)未声明便使用和同一作用域重复声明变量
//例子1:
function bar(x = y, y = 2) {//y还没有声明 return [x, y]; } //例子2:
let x = x;
//例子3:
function func(arg) {
let arg;
}
利用:
(1)利用作用域解决for循环变量在闭包中的引用问题:循环变量部分(for的括号部分是一个单独的作用域)是一个父作用域,循环体内部是一个单独的子作用域。
父子作用域:
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc
(2)解决for循环变量在闭包引用的问题:
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () {
console.log(i);//当前的i只在本轮循环有效,每一次循环的i其实都是一个新的变量 }; } a[6](); // 6
const
特点:同let
注意点:
(1)一旦声明变量,就必须立即初始化,不能留到以后赋值。
(2)一旦声明,变量指向的那个内存地址所保存的数据不得改动,即不能对这个变量整体重新赋值。
对于引用类型的数据(主要是对象和数组),const
只保证这个指针是固定的(即总是指向另一个固定的地址),但使用者可改变。因此,将一个对象声明为常量必须非常小心。
//数组同理 const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 改变指针:将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only
使用const声明的引用类型变量,声明者如果不想使用者更改对象属性,可以使用Object.freeze方法将对象冻结,添加新属性不起作用,严格模式时还会报错。
//将对象和对象的属性冻结 var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); };
let和const
建议优先使用const
,尤其是在全局环境,不应该设置变量,只应设置常量。
(1)const
可以提醒阅读程序的人,这个变量不应该改变;也防止了无意间修改变量值所导致的错误。
(2)JavaScript 编译器对const
进行了优化,所以多使用const
,有利于提高程序的运行效率。let和const
的本质区别,其实是编译器内部的处理不同。
字符串
使用字符串的常用姿势:
1、是否包含指定字符串,第二个参数,表示开始搜索的位置。
s.includes('Hello', 6) // false
2、模板字符串,用反引号(`)标识
(1)当普通字符串使用
(2)或使用${变量}在字符串中插入变量、表达式、或者调用函数
//变量,表达式 let x = 1; let y = 2; `${x} + ${y} = ${x + y}` // "1 + 2 = 3" //函数调用 function fn() { return "Hello World"; } `foo ${fn()} bar` // foo Hello World bar
(3)定义多行字符串,所有的空格和缩进都会被保留在输出之中,如果不想要这个换行,可以使用trim()。
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`.trim());
3、使用标签模板过滤 HTML 字符串,防止用户输入恶意内容。
//sender变量往往是用户提供的,经过SaferHTML函数处理,里面的特殊字符都会被转义。 let sender = '<script>alert("abc")</script>'; // 恶意代码 let message = SaferHTML`<p>${sender} has sent you a message.</p>`; function SaferHTML(templateData) { let s = templateData[0]; for (let i = 1; i < arguments.length; i++) { let arg = String(arguments[i]); // Escape special characters in the substitution. s += arg.replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">"); // Don't escape special characters in the template. s += templateData[i]; } return s; } message // <p><script>alert("abc")</script> has sent you a message.</p>
4、多语言转换(国际化处理)。
5、解构
const [a, b, c, d, e] = 'hello';
解构赋值
好好理解这句话:从数组或者变量提取对应key的值,对变量进行赋值,这被称为解构。
使用场景:数组,对象,字符串,函数参数、扩展运算符,模块加载import,set结构、map结构等具有Iterator 接口的数据结构。
建议:
(1)使用数组成员对变量赋值时,优先使用解构赋值。
(2)函数的参数如果是对象的成员,优先使用解构赋值。
(3)如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。
注意点:
(1)等号两边的模式要相同。左边数组,右边也要是数组。左边是对象,右边也要是对象
//报错
var a={x:1} var [x]=a
//正常
var {x}=a
(2)解构没找到匹配的值,变量的值就等于undefined
。
let [bar, foo] = [1];//foo:undefined
let { baz } = { foo: "aaa", bar: "bbb" };//baz: undefined
使用场景:
1、数组解构:见“数组解构,常用姿势”。
2、对象解构:见“对象解构,常用姿势”。
3、字符串解构:
const [a, b, c, d, e] = 'hello';
4、函数返回多个值,放在数组或者对象里返回:
// 返回一个数组 function example() { return [1, 2, 3]; } let [a, b, c] = example(); // 返回一个对象 function example() { return { foo: 1, bar: 2 }; } let { foo, bar } = example();
5、提取json数据:
let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData;
6、模块加载中的应用
// circle.js export function area(radius) { return Math.PI * radius * radius; } export function circumference(radius) { return 2 * Math.PI * radius; } // main.js import { area, circumference } from './circle';
console.log('圆面积:' + area(4)); console.log('圆周长:' + circumference(14));
7、遍历map(键值对的集合),其实也是一种解构
const map = new Map(); map.set('first', 'hello'); map.set('second', 'world'); for (let [key, value] of map) { console.log(key + " is " + value); } // first is hello // second is world
数组
常用方法:foreach,filter,map
数组使用场景:
1、数组解构,常用姿势:
(1)用于变量:完全解构、部分解构、剩余运算符、具有 Iterator 接口的数据解构;
let [a, b, c] = [1, 2, 3];
let [foo, [[bar], baz]] = [1, [[2], 3]];//foo:1,bar:2,baz:3
let [head, ...tail] = [1, 2, 3, 4]; //tail:[2, 3, 4],这里的扩展运算符可以理解为剩余运算符
let [x, y, ...z] = ['a'];//x:'a',y:undefined,z:[]
let [x, y, z] = new Set(['a', 'b', 'c']); //set结构的数据,使用...或者Array.from可以将其转为数组 x // "a"
具有 Iterator 接口(也就是可以用for...of遍历),都可以采用数组形式的解构赋值, 原生具备 Iterator 接口的数据结构有以下:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
注意:并不是所有类数组对象(特征只有一点:必须有length
属性)都具有 Iterator 接口,可以使用Array.from
方法将其转为数组。
(2)解构用于函数参数
function add([x=0, y=0]=[]){
//参数设置默认值一般用这种写法(左边参数设置默认值,右边设置类型),防止调用时没有传参报错。对象的解构也是function move({x = 0, y = 0}={}) return x + y; } add([1, 2]); // 3
//例子2:
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
//例子3:
[[1, 2], [3, 4]].map(([a, b]) => a + b);//[a,b]:数组单个索引元素解构
// [ 3, 7 ]
(3)在解构中给变量指定默认值
//变量指定默认值,在数组元素严格等于undefined时生效 let [x, y = 'b'] = ['a']; // x='a', y='b'
//默认值是一个表达式,当为undefined的时候才去求值(惰性求值) //因为x能取到值,所以函数f根本不会执行。 function f() { console.log('aaa'); } let [x = f()] = [1];
2、与扩展运算符配合使用:将数组转为用逗号分隔的参数序列,用于函数参数。
function add(x, y) { return x + y; } const numbers = [4, 38]; add(...numbers) // 42
3、使用扩展运算符(...)拷贝数组,注意是浅拷贝
// good const itemsCopy = [...items];
4、使用 Array.from 方法或者...,将类似数组的对象转为数组。
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);
对象
1、使用建议:
(1)单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
const a = { k1: v1, k2: v2 }; const b = { k1: v1, k2: v2, };
(2)对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign
方法。
const a = {};
Object.assign(a, { x: 3 });
(3)如果对象的属性名是动态的,在创造对象的时候,使用属性表达式定义,这样一来,所有属性就在一个地方定义了。而不是在后期去添加这个属性
const obj = { id: 5, name: 'San Francisco', [getKey('enabled')]: true, };
(4)对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。
const atom = { ref, value: 1, addValue(value) { return atom.value + value; }, };
(5)将现有对象的方法,赋值到某个变量,使用起来会方便很多。
let { log, sin, cos } = Math;
(6)使用扩展运算符合并两个对象,根据两个对象,可能是浅拷贝也可能是深拷贝,具体详情见下面Object.assign的使用
let ab = { ...a, ...b }; // 等同于 let ab = Object.assign({}, a, b);
2、Object.assign的使用
将源对象(source)的所有可枚举属性,复制到目标对象(target)。
语法:
Object.assign(target, ...sources)
//如:Object.assign(target, source1, source2); 将1和2合并,会生成一个全新的对象那个,赋值给target,target修改属性时,不影响原来的对象。
注意点:
语法中只有一个原对象时,如果原对象里面还存在引用类型的数据,当在target上修改该引用数据,原对象的该数据也会被更改。即这只是浅拷贝
let obj = { name: '程序猿', age:{child: 12} } let copy = Object.assign({}, obj); copy.name = '单身狗' copy.age.child = 24 console.log(obj) // { name: '程序猿', age:{child: 24} }
解决上述问题,解决原理是设置多个对象的合并,返回一个全新的对象赋值给目标对象。两种实现方式:
(1)针对要修改的属性,定义单独的变量,合并到源对象,再赋值目标对象。上面例子即定义单独的age。
let obj = {name: '二月', age: {c: 12}} let age = {c: 88} let o2 = Object.assign({}, obj, {age}) o2.age.c = 66 console.log(obj, o2)
(2)对原对象使用扩展运算符,对要修改的属性,在后面单独写出,覆盖原对象对应的属性
let obj = {name: '二月', age: {c: 12}} let o1 = {...obj, age: {c: 88}} o1.age.c = 99 console.log(obj, o1)
使用场景:
(1)上述注意点实现了对象的拷贝。
(2)一次向类添加多个新方法。
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
(3)使用扩展运算符实现的是浅拷贝
var a={x:1,y:2} var c={...a}//c:{x:1,y:2} c.x=3; a.x//1
3、对象解构,常用姿势:原理是先找到同名属性,然后再赋给对应的变量。
(1)用于变量
let { foo, bar ,f} = { foo: "aaa", bar: "bbb" }; //foo: "aaa", bar: "bbb",f:undefined
//如果变量名与属性名不一致,必须写成下面这样。 let obj = { first: 'hello', last: 'world' }; let { first: f, last: l } = obj; //first as f,可以这样理解first当做f输出。f:'hello'。这种写法时,first称为模式,f是变量
//解构嵌套结构的对象 let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p: [x, { y }] } = obj;//x :"Hello",y : "World"
(2)解构中指定默认值
//变量指定默认值,在对象的属性值严格等于undefined时生效。默认值为表达式或者函数,同数组一样式惰性求值 var {x, y = 5} = {x: 1};//x :1 y :5
(3)用于函数参数
function move({x = 0, y = 0} = {}) { return [x, y]; } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, 0] move({}); // [0, 0] move(); // [0, 0]
Reflect对象与对象
Reflect提供操作对象的新 API。
该对象方法:
(1)Reflect.get(target, name, receiver) ,如果name
属性部署了读取函数(getter),则读取函数的this
绑定receiver
。如果第一个参数不是对象,Reflect.get
方法会报错。
(2)Reflect.set(target, name, value, receiver)
(3)Reflect.has(obj, name):对应name in obj
里面的in
运算符。
(4)Reflect.deleteProperty(obj, name):等同于delete obj[name]
,用于删除对象的属性。如果删除成功,或者被删除的属性不存在,返回true
;删除失败,被删除的属性依然存在,返回false
。
(5)Reflect.construct(target, args):一种不使用new
来调用构造函数的方法。
// new 的写法 const instance = new Greeting('张三'); // Reflect.construct 的写法 const instance = Reflect.construct(Greeting, ['张三']);
(6)Reflect.setPrototypeOf(obj, newProto):设置目标对象的原型(prototype),返回一个布尔值,表示是否设置成功。
(7)Reflect.apply(func, thisArg, args):用于绑定this
对象后执行给定函数。
(8)Reflect.defineProperty(target, propertyKey, attributes):用来为对象定义属性,等同于Object.defineProperty
。
(9)Reflect.ownKeys (target):返回对象的所有属性,基本等同于Object.getOwnPropertyNames
与Object.getOwnPropertySymbols
之和。
函数
1、指定参数默认值,每次调用函数,对应参数为undefined时,默认值都重新计算。
注意:定义了默认值的参数为函数的尾参数,如果非尾部的参数设置默认值,这个参数省略时会报错。
好处:
(1)非尾部参数定义了默认值,在这个参数省略时会报错,这让使用者知道这些参数是可省略哪些参数是不可省略的,不用查看函数体或文档。
(2)当省略了某个不可省略的参数,利用默认参数抛出错误。
//如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。 function throwIfMissing() { throw new Error('Missing parameter'); } //当省略参数mustBeProvided 时,调用抛错函数 function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo()// Error: Missing parameter
(3)将来代码的优化,即使彻底拿掉可省略的参数,也不会导致代码无法运行。
2、调用函数时,参数形成一个单独的作用域,函数体内部的同名局部变量
不
影响。
let x = 1; function f(y = x) {//如果此时,全局变量x不存在,就会报错。 let x = 2; console.log(y); } f() // 1
3、rest参数(形式为...变量
)
注意点:
(1)变量在函数体中是一个真正的数组,所以可以使用数组的方法。
(2)rest 参数之后不能再有其他参数,否则会报错。
(3)函数的length属性,预期传入的参数个数,被指定默认值的参数或者rest 参数不被包含在内。
(4)扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式
4、箭头函数
书写注意点:
(1)不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
(2)箭头函数返回一个对象,必须在对象外面加上括号,否则会报错。
(3)代码块部分多于一条语句,使用大括号将它们括起来,使用return
返回结果。
使用注意点:
(1)函数体内的this
对象,是定义时所在的对象,而不是使用时所在的对象,是不可变的。
//例子1:
function foo() { setTimeout(() => {//箭头函数的定义是在foo函数调用时,此时this是foo的。所以箭头函数执行时,this是foo的this
console.log('id:', this.id); }, 100); } var id = 21; foo.call({ id: 42 });// id: 42
//例子2: var handler = { id: '123456', init: function() { document.addEventListener('click', event => this.doSomething(event.type), false);//在调用handler.init时,作为回调的箭头函数被定义,此时this总是指向handler对象。 }, doSomething: function(type) { console.log('Handling ' + type + ' for ' + this.id); } };
(2)不要在箭头函数里用this的场景:
//场景1:定义对象的方法时方法内部this指向window。 const cat = { lives: 9, jumps: () => { this.lives--;//this指向全局对象 } } //场景2:需要动态this的时候,也不应使用箭头函数。 var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on');//this指向的是window });
(3)不可以当作构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误。
(4)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(5)不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
(6)由于箭头函数没有自己的this
,所以不能用call()
、apply()
、bind()
这些方法去改变this
的指向
(7)使用双冒号运算符,显示绑定箭头函数的this。
a、双冒号左边对象,右边函数,则左边的对象作为上下文环境(即this
对象)绑定到右边的函数上面。
b、双冒号左边为空,右边是对象的方法,则等于将该方法绑定在该对象上面。
c、如果双冒号运算符的运算结果还是一个对象,就可以采用链式写法。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
箭头函数常用姿势:
(1)立即执行函数写成箭头函数的形式。
(() => { console.log('Welcome to the Internet.'); })();
(2)需要使用函数表达式的场合,尽量用箭头函数代替,因为这样更简洁。
// best [1, 2, 3].map(x => x * x);
5、函数的写法建议:
(1)如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。
(2)所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
function divide(a, b, { option = false } = {}) { }
(3)不要在函数体内使用 arguments 变量,使用 rest 运算符(...)代替。arguments 是一个类似数组的对象,而 rest 运算符可以提供一个真正的数组。
(4)使用默认值语法设置函数参数的默认值。
set和map构造函数
Set 构造函数,实例类似于数组。
Map构造函数,实例类似于对象。区别在于可以使用对象作为键名,注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
它们的成员通过===判断后,去掉重复的成员,所以可以用于数组或者对象的去重,再通过一定方式转回真正的数组和对象。
实例的属性和方法:
1、属性:size,成员数
2、方法:分为操作数据和遍历数据两种类型
(1)set
操作:
add(value)
:添加某个值,返回实例本身,可链式调用。
delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。
has(value)
:返回一个布尔值,表示该值是否为Set
的成员。
clear()
:清除所有成员,没有返回值。
遍历(由于 Set 结构没有键名,只有键值(即键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致):
keys()
:返回键名的遍历器(遍历器可以理解为类数据,可用for...of遍历)
values()
:返回键值的遍历器
entries()
:返回键值对的遍历器
forEach()
:使用回调函数遍历每个成员,用于对每个成员执行某种操作,没有返回值。函数参数依次为键值、键名、集合本身。forEach方法还可以有第二个参数,绑定处理函数内部的this
对象。
Set实例默认可遍历,所以keys,values,entries平时使用意义不大。
let set = new Set(['red', 'green', 'blue']); for (let item of set) { console.log(item); } //red //green //blue
(2)map
操作:
set(key, value):返回整个 Map 结构,因此可以采用链式写法。
get(key):找不到key
,返回undefined
。
has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中。
delete(key):删除某个键,返回true
。如果删除失败,返回false
。
clear():清除所有成员,没有返回值。
遍历:
Map 的遍历顺序就是插入顺序。
keys()
:返回键名的遍历器。
values()
:返回键值的遍历器。
entries()
:返回所有成员的遍历器。
forEach()
:第一个参数回调函数,函数参数(value, key, map),接受第二个参数,用来绑定函数中的this
。
Map 结构的默认遍历器接口就是entries
方法。
for (let [key, value] of map) { console.log(key, value); } // "F" "no" // "T" "yes"
类
1、类里面的元素:构造函数,实例属性,原型方法,静态方法,静态属性
let methodName = 'getArea';
class Point {
_count = 0;//实例属性可以定义在类的最顶层,相当于: this._count = 0; constructor(x, y) { //构造函数 this.x = x;//实例属性 this.y = y; } toString() {//原型的方法 return '(' + this.x + ', ' + this.y + ')'; }
[methodName]() {//类的属性名,可以采用表达式。
// ...
}
static bar() {//静态方法不会被实例继承,可以被子类继承,直接通过类调用。静态方法内this指的是类,而不是实例。
this.baz();
}
} Point.prop = 1; //静态属性,Class本身的属性,不在实例对象上。目前,只有这种写法 var b = new Point(1,2);//new的过程和es5一样
2、对类的操作
(1)给类添加新方法:Object.assign
方法一次向类添加多个方法。参见对象的Object.assign方法
(2)在“类”的内部使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass { constructor() { // ... } get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); } } let inst = new MyClass(); inst.prop = 123;// setter: 123 inst.prop// 'getter'
(3)使用new.target
定义不能独立使用、必须继承后才能使用的类。
new.target特点:
a、new.target只能在类内部调用
,返回当前 Class。
b、子类继承父类时,new.target
会返回子类。
c、如果构造函数不是通过new
命令或Reflect.construct()
调用的,new.target
会返回undefined。
class Shape { constructor() { console.log(new.target === Rectangle); } } class Rectangle extends Shape { constructor(length, width) { super(); // ... } } var x = new Shape(); // 报错 var y = new Rectangle(3, 4); // 输出true,正确
(4)mix函数,将多个对象合为一个类,使用时,只要继承这个类即可
function mix(...mixins) { class Mix { constructor() { for (let mixin of mixins) { copyProperties(this, new mixin()); // 拷贝实例属性 } } } for (let mixin of mixins) { copyProperties(Mix, mixin); // 拷贝静态属性 copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性 } return Mix; } function copyProperties(target, source) { for (let key of Reflect.ownKeys(source)) { if ( key !== 'constructor' && key !== 'prototype' && key !== 'name' ) { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } } //使用 class DistributedEdit extends mix(Loggable, Serializable) { // ... }
(5)使用export
关键字输出外部能够读取的模块内部变量(变量,函数,类)。
3、类的注意点:
(1)类不存在变量提升(hoist),这种规定的原因与继承有关,必须保证子类在父类之后定义。
(2)类的继承注意点:
a、子类通过extends
关键字继承了父
类的所有属性和方法。
b、super在子类中作为函数使用,它代表父类的构造函数,只能且必须在在子类构造函数中调用,返回子类的实例,即此时父类构造函数内部的
this
指的是子类的实例。
为什么要调用super:子类的this
通过父类的构造函数(super)得到父类实例的属性和方法,然后再对其加上子类自己的实例属性和方法。因此子类实例的构建,是基于父类实例。
c、super:
在子类原型方法中作为对象使用,它代表父类的原型,调用父类的方法时,父类方法内部的this
指向当前的子类实例。(下面提到,原型方法尽量不要有this,到时候扯不清)
在子类静态方法中,它代表父类,调用父类的静态方法时,方法内部的this
指向当前的子类,而不是子类的实例。
(3)this 的指向
a、原型方法内部如果含有this
,它默认指向类的实例。如果在外部单独用到类的这个方法,很可能报错。尽量不要在原型的方法中使用this。
b、静态方法里的this
指向类,而不是实例。
c、继承中的this,容易晕,所以除了构造函数中使用this,静态方法或者原型方法中尽量不要用this。
Proxy构造函数
在语言层面,为修改对象某些操作的默认行为而提供新 API,用来定制拦截行为。
//语法 var proxy = new Proxy(target, handler); //例子: var proxy = new Proxy({}, { get: function(target, property) { return 35; } });
使用技巧:将 Proxy 对象设置到object.proxy
属性,从而可以在object
对象上调用实例。
var object = { proxy: new Proxy(target, handler) };
Proxy 支持的拦截操作一共 13 种:详细参见文档。
get,set,apply,has,construct,deleteProperty,defineProperty,getOwnPropertyDescriptor,getPrototypeOf,isExtensible,ownKeys,preventExtensions,setPrototypeOf
使用场景待收集。
模块
1、现有的模块方案:ES6,CommonJS 和 AMD。
CommonJS 和 AMD:前者用于服务器,后者用于浏览器。ES6可取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6与CommonJS 加载原理区别;
ES6 一个模块就是一个独立的文件,通过export
命令显式指定输出变量,再通过import
命令输入变量,外部无法直接获取变量。在编译时就完成模块加载,确定模块的依赖关系,以及输入和输出的变量,效率要比 CommonJS 模块的加载方式高。
但也导致无法在运行时加载模块,import
命令无法取代require
的动态加载功能。
CommonJS 模块就是对象,运行时生成对象,输入时查找对象属性。只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
2、ES6 的模块自动采用严格模式
严格模式主要有以下限制。
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象,尤其需要注意this
的限制。ES6 模块之中,顶层的this
指向undefined
- 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
3、模块输入输出使用姿势:
(1)当import的是export输出的变量时,import命令使用大括号,里面指定要从其他模块导入的变量名。变量名必须与被导入模块(profile.js
)对外接口的名称相同。如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
(2)当import的是export default输出的变量时,import
命令可以为该匿名函数指定任意名字,这时import
命令后面,不使用大括号。
(3)import的是混合输出时
import _, { each, forEach } from 'lodash';
(4)使用import语句不输入任何值,则执行所加载的模块。
(5)模块输出整体加载,即除了指定加载某个输出值,还可以使用星号(*
)指定一个对象,某个模块的所有输出值都加载在这个对象上面。(忽略模块的
default
方法)
(6)在一个模块中,输出即输入的写法(export 与 import 的复合写法)用于模块的继承,注意:foo和bar
实际上并没有被导入当前模块,相当于对外转发了这两个接口,当前模块不能直接使用foo
和bar
。
export { foo, bar } from 'my_module'; // 接口改名 export { foo as myFoo } from 'my_module'; // 整体输出 export * from 'my_module'; //默认接口 export { default } from 'foo'; //具名接口改为默认接口 export { es6 as default } from './someModule'; // 等同于 import { es6 } from './someModule'; export default es6; //默认接口也可以改名为具名接口 export { default as es6 } from './someModule';
模块的继承写法:
假设有一个circleplus
模块,继承了circle
模块。在另外一个文件中引入circleplus模块的所有输出(包含了继承来的)
// circleplus.js export * from 'circle'; export var e = 2.71828182846; export default function(x) { return Math.exp(x); } //加载circleplus模块,将circleplus模块的默认方法加载为exp方法。 // main.js import * as math from 'circleplus'; import exp from 'circleplus'; console.log(exp(math.e));
(7)跨模块常量的使用和管理
const声明的常量只在当前代码块有效。如果向一个值被多个模块共享,可以使用export导出,其他模块引入。
如果要使用的常量非常多,可以建一个专门的constants
目录,将各种常量写在不同的文件里面,保存在该目录下。
// constants/db.js export const db = { url: 'http://my.couchdbserver.local:5984', admin_username: 'admin', admin_password: 'admin password' }; // constants/user.js export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator']; //将这些文件输出的常量,合并在index.js里面。 // constants/index.js export {db} from './db'; export {users} from './users'; //使用的时候,直接加载index.js就可以了。 // script.js import {db, users} from './constants/index';
(8)浏览器加载es6模块,等同于设置了<script>
标签的defer
属性,异步加载,延迟执行。即等到整个页面渲染完,再执行模块脚本,不会造成堵塞浏览器,执行是按照在页面出现的顺序依次执行
<script type="module" src="./foo.js"></script>
4、注意点:
(1)export命令后根变量声明而不是变量表达式。
更改输出变量名:暴露出forEach
接口,默认指向each
接口,即forEach
和each
指向同一个方法。(没必要用么绕)
export function each(obj, iterator, context) { // ··· } export { each as forEach };
(2)export命令输出的是变量实时的值。
(3)export命令只能处于模块顶层,块级作用域内,会报错。
(4)一个模块只能有一个export default
命令,本质上,export default
就是输出一个叫做default
的变量或方法,它后面不能跟变量声明语句。
(5)因为export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
(6)import命令输入的变量都是只读的,对其重新赋值就会报错,如果是一个对象,改写其
属性是允许的。由于其他模块也可以读到改写后的值。建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。
(7)import后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
(8)模块之中加载模块,使用import
命令指定路径时,.js
后缀不可省略,需要提供绝对 URL 或相对 URL。
(9)import命令具有提升效果,会提升到整个模块的头部,首先执行。这种行为的本质是,import
命令是编译阶段执行的,在代码运行之前。所以最好把import集中写在文件顶部。
(10)import不能使用表达式和变量这些只有在运行时才能得到结果的语法结构,因为import
是静态执行(编译时),在静态分析阶段,这些语法都是没法得到值的。
// 报错,表达式 import { 'f' + 'oo' } from 'my_module'; // 报错,变量 let module = 'my_module'; import { foo } from module; // 报错,if结构 if (x === 1) { import { foo } from 'module1'; } else { import { foo } from 'module2'; }
(11)使用import对模块整体加载,在引用的文件中,不允许改变这个对象。
import * as circle from './circle'; // 下面两行都是不允许的 circle.foo = 'hello'; circle.area = function () {};
(11)输出即输入写法中,当前模块相当于对外转发了接口,当前模块不能直接使用引入的接口
。
(12)代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
(13)模块脚本自动采用严格模式,不管有没有声明use strict
。
(14)模块之中,顶层的this
关键字返回undefined
,而不是指向window
。利用顶层的this
等于undefined
这个语法点,可以侦测当前代码是否在 ES6 模块之中。
(15)同一个模块如果加载多次,将只执行一次。
异步
异步编程(请求,定时器,事件等)传统的解决方案——回调函数和事件。
回调缺点:容易层层嵌套。
事件缺点:如果你错过了它,再去监听,是得不到结果的。
es6中新增异步处理方案,内容包含promise,generator,async。
promise
优点:编程方式上避免了层层嵌套的回调函数,如果事件发生时错过了,promise新增的回调也能得到该事件的结果。(使用直接看下面的使用建议,不是支撑特别强大的功能,没必要使用得那么复杂)
特点:
(1)有三种状态:pending
(进行中)、fulfilled
(对应resolve,已成功)和rejected
(已失败)。异步返回的结果(成功,失败)决定当前是哪一种状态,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个,即再对Promise
对象添加回调函数,会立即得到这个改变的结果。
(3)Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。
1、构造函数
(1)使用:接受一个函数作为参数,new的时候就执行函数代码(// ... some code),根据操作结果(成功或者失败),手动调用函数参数的resolve和reject方法。
const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });
(2)构造函数的方法(all,race,resolve,reject,try)
Promise.all():
参数:成员是promise实例的数组(可以不是数组,但必须具有 Iterator 接口,成员可不是promise实例)。
作用:将多个 Promise 实例包装成一个新的 Promise 实例,新实例的状态:
resolve:所有实例都resolve时,每个实例返回值组成一个数组,传递给新实例的回调。
const databasePromise = connectDatabase(); const booksPromise = databasePromise .then(findAllBooks); const userPromise = databasePromise .then(getCurrentUser); Promise.all([ booksPromise, userPromise ]) .then(([books, user]) => pickTopRecommendations(books, user));
上面代码中,booksPromise
和userPromise
是两个异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommendations
这个回调函数。
用于不互相依赖的多个异步操作。使用见“使用建议”。
promise.race():
作用同Promise.all()。状态的改变:只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
Promise.resolve():
将参数转为 Promise 对象返回(此时状态为resolve),参数分成四种情况:Promise 实例,具有then
方法的对象,不是具有then
方法的对象或根本就不是对象,不带有任何参数。
Promise.reject():
返回一个Promise 实例,该实例的状态为rejected
。该方法的参数作为实例reject回调
的参数。参数是一个具有then
方法的对象时,实例的catch
方法的参数是thenable
对象。
Promise.try():
在使用then方法管理流程时,不管它的回调是同步还是异步,最好都用Promise.try
包装一下,统一用promise.catch()
捕获所有同步和异步的错误。
Promise.try(() => database.users.get({id: userId})) .then(...) .catch(...)
2、实例
实例为一个promise对象,方法有:then,catch,fainally。
(1)then方法
a、成功函数接收resolve传递的异步操作的结果作为参数,一般来说,不要在then
方法里面定义 Reject 状态的回调函数,总是使用catch
方法。then方法指定的回调函数,如果运行中抛出错误,也会被catch
方法捕获。
promise.then(function(value) { // success }) .catch(function(error) { console.log(error) });
b、then方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法,第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。如果参数是一个promise对象(即有异步操作,依赖前一个异步的结果),下一个then等待该Promise
对象的状态发生变化,才会被调用。
(2)catch方法
a、catch方法返回一个 Promise 对象,因此后面还可以接着调用then
方法。运行完catch
方法指定的回调函数,会接着运行后面那个then
方法指定的回调函数。如果没有报错,则会跳过catch
方法,执行后面的then
b、Promise 对象的错误会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch
语句捕获。
下面代码中,一共有三个 Promise 对象:一个由getJSON
产生,两个由then
产生。它们之中任何一个抛出的错误,都会被最后一个catch
捕获。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
c、使用多个catch,前一个catch方法之中抛出错误,后面的catch捕获前一个catch
方法抛出的错误。
(3)finally方法
不管 Promise 对象最后状态如何,都会执行的操作。该方法的回调函数不接受任何参数,这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally
方法关掉服务器。
server.listen(port) .then(function () { // ... }) .finally(server.stop);
注意点:
(1)Promise new的同时立即执行
let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved
(2)then方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例),采用链式写法时,前一个then的第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。有可能返回的还是一个Promise
对象(即有异步操作,接口的连续调用),这时后一个回调函数,就会等待该Promise
对象的状态发生变化,才会被调用。
(3)如果 Promise 状态已经变成resolved
,再抛出错误是无效的。
const promise = new Promise(function(resolve, reject) { resolve('ok'); throw new Error('test'); }); promise .then(function(value) { console.log(value) }) .catch(function(error) { console.log(error) }); // ok
(4)在Promise.all的使用中,如果作为参数的 Promise 实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。
使用建议:
(1)一般来说,不要在then
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法。
(2)使用Promise.all()用于同时调用多个不相互依赖的接口,他们的结果共同给某个处理函数用,解决了以前等有了响应再调另外一个请求的情况,例子见promise.all方法
(3)Promise.race():作用同Promise.all()。状态的改变:只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
(4)一般的异步操作,使用一个then和catch。
(5)reject函数的参数通常是Error
对象的实例,表示抛出的错误;
(6)调用resolve
或reject
并不会终结 Promise 的参数函数的执行。一般来说,调用resolve
或reject
以后,Promise 的使命就完成了,后继操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。
使用举例:
(1)异步加载图片:
function loadImageAsync(url) { return new Promise(function(resolve, reject) { const image = new Image(); image.onload = function() { resolve(image); }; image.onerror = function() { reject(new Error('Could not load image at ' + url)); }; image.src = url; }); }
(2)封装ajax:
const getJSON = function(url) { const promise = new Promise(function(resolve, reject){ const handler = function() { if (this.readyState !== 4) { return; } if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; const client = new XMLHttpRequest(); client.open("GET", url); client.onreadystatechange = handler; client.responseType = "json"; client.setRequestHeader("Accept", "application/json"); client.send(); }); return promise; }; getJSON("/posts.json").then(function(json) { console.log('Contents: ' + json); }, function(error) { console.error('出错了', error); });
(3)读取文件:把fs
模块的readFile
方法包装成一个 Promise 对象,手动执行 Generator 函数,手动执行其实就是用then
方法,层层添加回调函数。
具体实现参看Generator中的“使用第三方库来实现generator函数的异步/同步流程管理”。
Generator
1、Generator的写法和调用:
调用 Generator 函数后,该函数并不执行,返回一个遍历器对象。通过调用遍历器对象的next方法,开始函数的执行。执行过程:
function* helloWorldGenerator() { yield 'hello'; console.log(11); yield 'world'; console.log(22); return 'ending'; console.log(33); } var hw = helloWorldGenerator(); hw.next()// { value: 'hello', done: false },从函数头部执行到第一个yield hw.next()//11, { value: 'world', done: false },从console.log(11)开始执行,直到返回yield ‘word’ hw.next()//22, { value: 'ending', done: true } hw.next()// { value: undefined, done: true }
2、遍历器对象的方法
next():
该方法可接收一个参数,该参数就会被当作上一个yield
表达式的返回值。也就是可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
看一个例子,可以在foo里打断点,了解具体的执行过程和里面几个变量的变化过程。
function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5);//x为5 a.next() //输出第一个yield的值:从函数头部执行到第一个yield,此时为x+1,即value为6. {value: 6, done: false} a.next(8) //输出第二个yield的值:y=2*8=16,y的值变为16,yield (16/3)=5.3333333 {value: 5.33333, done: false} a.next(4) //指针到了return语句:z的值变为4,输出return的值:5+16+4=25 {value: 25, done: true}
throw():
(1)在函数体外通过遍历对象调用throw, Generator 函数体内的catch捕获throw的异常,接收throw的参数。如果 Generator 函数内部没有部署try...catch
代码块,那么throw
方法抛出的错误,将被外部try...catch
代码块捕获。
(2)throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法。
(3)throw方法被捕获以后,会附带执行下一条yield
表达式。
var g = function* () { try { yield; } catch (e) { console.log('内部捕获', e); } }; var i = g(); i.next(); try { i.throw('a');//第一个错误被 Generator 函数体内的catch语句捕获。 i.throw('b');//第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。 } catch (e) { console.log('外部捕获', e); } // 内部捕获 a // 外部捕获 b
return():
参数作为返回的value的值,并且终结遍历 Generator 函数。
function* gen() { yield 1; yield 2; yield 3; } var g = gen(); g.next() // { value: 1, done: false } g.return('foo') // { value: "foo", done: true } g.next() // { value: undefined, done: true }
3、注意点
(1)遇到yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。
下面例子中,第一次调用next,从函数头部执行到第一个yield求值,但是并不会执行console.log(`1. ${yield}`); 这个属于第二个next要执行的代码了。
function* dataConsumer() { console.log('Started'); console.log(`1. ${yield}`); console.log(`2. ${yield}`); return 'result'; } var genObj = dataConsumer();
4、使用场景
1、异步操作不需要写回调:
把异步操作写在yield
表达式里面,结果赋值给一个变量,后续操作可以放在yield
表达式下面(可以直接使用异步结果变量),等到异步操作完成,在异步回调调用next
方法往后执行。
例子1:loading,这种写法的好处是所有Loading
界面的逻辑,都被封装在一个函数,按部就班非常清晰。
function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); loader.next()// 加载UI
loader.next()// 卸载UI
例子2:Ajax 操作用同步的方式表达。
function* main() { var result = yield request("http://some.url"); var resp = JSON.parse(result); console.log(resp.value); } function request(url) { makeAjaxCall(url, function(response){ it.next(response);//参数作为yield的值 }); } var it = main(); it.next();
例子3:逐行读取文本文件。
function* numbers() { let file = new FileReader("numbers.txt"); try { while(!file.eof) { yield parseInt(file.readLine(), 10); } } finally { file.close(); } }
2、使用第三方库来实现generator函数的异步/同步流程管理
Thunkify 模块或者co模块,参看文档。
Async函数
用于异步操作,await
后面为具体的异步操作(一般是一个Promise 对象,返回该对象的结果)。
调用即执行,返回值是 Promise对象,可以用then
方法指定下一步的操作。难点是错误处理机制。
async函数完全可以看作将多个异步操作包装成的一个 Promise 对象:
只有async
函数内部的所有异步操作执行完,返回的 Promise对象才会发生状态改变,才会执行then
方法指定的回调函数。
函数内部return
语句返回的值(多个await后面promise的值组成对象或者数组返回),会成为then
方法回调函数的参数。即使前一个异步操作失败,也不会中断后面的异步操作。
任何一个await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行。
可以与promise的all方法对应起来理解。
使用建议:
1、关于错误的处理
(1)两个await的时候,可以将第一个await
放在try...catch
结构里面,即使前一个异步操作失败,也不中断后面的异步操作。
//方法一:将第一个await放在try里 async function f() { try { await Promise.reject('出错了'); } catch(e) { } return await Promise.resolve('hello world'); } //方法二:第一个await的promise跟一个catch async function f() { await Promise.reject('出错了') .catch(e => console.log(e)); return await Promise.resolve('hello world'); }
(2) 如果有多个await
命令,可以统一放在try...catch
结构中。
async function main() { try { const val1 = await firstStep(); const val2 = await secondStep(val1); const val3 = await thirdStep(val1, val2); console.log('Final: ', val3); } catch (err) { console.error(err); } }
2、多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发,提升性能。
//相继触发,getFoo完成以后,才会执行getBar let foo = await getFoo(); let bar = await getBar(); //同步触发,因为在await前就已经先调用了 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise;
使用场景:
依次远程读取一组 URL,然后按照读取的顺序输出结果,并发发出远程请求:
async function logInOrder(urls) { // 并发读取远程URL const textPromises = urls.map(async url => { const response = await fetch(url); return response.text(); }); // 按次序输出 for (const textPromise of textPromises) { console.log(await textPromise); } }