速通JS Day1-7
John_Smith · · 科技·工程
Learn From Scratch
开头使用"use strict"来使用ECMA5及以后的标准。
最好在每一个语句之后加上分号;。
使用let, const而非var!!!!!(let有高贵的块级作用域,没有声明提升 hoisting)
HTML里面可以引用外部脚本文件(下载到缓存),但是标签需要闭合:
<script src="relative_or_absolute_path/script.js"></script>
数据类型:Number(±Infinity NaN)、BigInt±(253-1)、String(格式字符串的使用反引号${})、Boolean、null(无、空、值未知)、undefined(未被赋值)、Object、Symbol。可以强制类型转换。
数字转换:
| 值 | 变成…… |
|---|---|
undefined |
NaN |
null |
0 |
true / false |
1 / 0 |
string |
“按原样读取”字符串,两端的空白字符(空格、换行符 \n、制表符 \t 等)会被忽略。空字符串变成 0。转换出错则输出 NaN。 |
使用 typeof 来查询数据类型。注意的Feature:null->class function(本应为class)->function
交互方法:alert(msg), prompt(msg,[initial_input]), confirm(question)
只要有一个字符串,+变为concatenate,单元的+可以等同为转换为数字。优先级一元>二元(幂>乘除>加减)
字符串比较为Unicode字典序,数字比较显然,不同类型转化为Number再比较。严格相等/不等不转换类型。(严格运算为三个字符,不严格运算为两字符)
除了 null和undefined:
两者之间:null==undefined, null!==undefined(官方CP,null 只与 undefined 互等);在与其他东西比较的时候都会转化为Number。NaN与别的数字比较总会是False!
布尔转换(除了"\t0\n"之类的都相当于->Number->Boolean):
- 数字
0、空字符串""、null、undefined和NaN都会被转换成false。因为它们被称为“假值(falsy)”。 - 其他值被转换为
true,所以它们被称为“真值(truthy)”。注意所有的对象(即使没有任何属性)都是true。 JS有三目运算符。&&,||均惰性求值(返回值为原始值而非Boolean)。a??b等价于(a !== null && a !== undefined) ? a : b。(null与undefined为未定义值) 循环类型:while/do.while/for(;;)/for.of(iterate_array)/for.in(iterate_attr)。循环可以加上tagtag:for(;;)做到多层的break/continue:break/continue tag;。 存在switch-case-[break]-default语句(case可以为变量),比较时执行===。 函数是第一公民。函数表达式可以认为是λ函数(但是可以有自己的名称,可以副职,匿名赋值时末位加分号,且运行时即刻创建/销毁)。函数可以作为参数。 函数在严格模式下具有块作用域。预处理时便直接声明(不用考虑先后顺序)。 好吧对不起箭头函数才是λ函数。let func = (a, b) => a + b; //剪头后也可以插入代码块总结
Object
Basic
JS定义对象的语法非常反人类。(定义类的语法更是一坨,直到ES6才有)
类似Python,JS的key(必须是字符串,但是无空格的字符串使用的是自然写法)-value可以随时创建(对于实例直接赋值)与删除(delete Instance.attr),你甚至可以使用多个单词的key(要求使用""包括,引用时使用Object["String"]——这个用法对于单个单词的key也适用,下文中user["name"]也是合法的)。
对象的属性名甚至可以使用保留字,或者是数字等等(在找key的情况下,所有[]里面的东西都会被自动转化为字符串,但你依然可以使用纯数字+方括号来索引)。
let key = "double words"
let attr = prompt("Please input a string: ")
let user = {
name: "John",
age: 30, //建议最后的属性要带上逗号
[attr]: undefined, //这种带中括号的即 计算属性,可以直接算出来中括号中的值作为key
}
user[key] = true;
定义的时候可以使用属性值简写,有点像构造函数:
function makeUser(name, age) {
return {
name, // 与 name: name 相同
age, // 与 age: age 相同
// ...
};
}
但是不一定要使用函数的形式,在普通的let语句中也可以这样用(别害怕,找不到的时候会给你undefined的)。判断是否有某个属性的方法为key in Object。
对于对象,可以使用for.in来遍历key:
for(let key in Obj) {
alert(`key-value: ${key}-${Obj[key]}`);
}
排列顺序:整数属性会排序,剩下的按照定义顺序。
整数属性:先强制转换为
Number再强制转换为String与原来相同的key("49"是,"+49","1.2"均不是)。
为了防止排序,你的key可以写成"+Integer"的形式。这样就会被认为这是一个字符串。
依然类似Python,任何mutable的东西(比如对象)都是引用赋值,共享地址。
因此,比较对象时,当且仅当地址相等,==, ===的结果才是True!(类似is)
如果你需要拷贝对象:一、使用for.in挨个赋值;二、Object.assign(target, src1, src2, ...)。
如果你想要deepclone:你得调库,或者递推地拷贝。
所有不可达的对象都会被自动垃圾回收。
方法定义:(在类继承时会有区别)
user = {
sayHi: function() {
alert("Hello");
}
};
// 方法简写看起来更好,对吧?
let user = {
sayHi() { // 与 "sayHi: function(){...}" 一样
alert("Hello");
}
};
可选链:value?.prop返回 value既不是null也不是undefined?value.prop:undefined。
而且还有 ?.(),?.[] 的变体。总结就是:只要左边是undefined就会直接短路。
典中典之this
JS中任何东西都会有this。对于类方法,this就是当前对象(当你要调用属性的时候必须使用this)。
所有的this都是在运行的时候即时算出来的。 没有this绑定。箭头函数没有this。
let user = { name: "John" };
let admin = { name: "Admin" };
function sayHi() {
alert( this.name );
}
// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;
// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)
admin['f'](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)
sayHi = function() {
alert(this);
}
sayHi(); // undefined(严格模式,若非严格模式得到全局对象),且若使用 this.name 会报错
如果想要链式调用(调用方法后产生一个可以使用的对象),你可以参考以下例子:
let ladder = {
step: 0,
up() {
return {
let newObj;
Object.assign(newObj, this);
newObj.step++;
return newObj;
}//这种是不希望原来的函数更改
/* 如果希望更改;{this.step++; return this;} 可以省内存*/
},
showStep() {
alert(`step=${this.step}`);
return this;
}
}
建议参考后文的习题来进一步学习。
幽默构造函数
构造函数并非定义在类中(或者是类方法)!所有构造函数都会是普通函数。
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack"); //new可能会迟到,但绝对不会缺席
//如果没有参数,可以省略括号(new User),但不建议这样写
alert(user.name); // Jack
alert(user.isAdmin); // false
//等价的写法: user = new function(){this.name = name;this.isAdmin = false;}
new.target:
function User() {
alert(new.target);
}
// 不带 "new":
User(); // undefined
// 带 "new":
new User(); // function User { ... }(返回该函数)
// 顺带一提,new Obj(...)[...]的语法是合法的
这样可以提供一种不需要new的神奇方法(库中常用,但是要慎用):
function User(name) {
if (!new.target) { // 如果你没有通过 new 运行我
return new User(name); // ……我会给你添加 new
}
this.name = name;
}
let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John
一般来说,构造函数是不需要return。但是如果有return,当且仅当返回对象时返回此对象(并忽略this),否则返回原始类型时忽略return。
Symbol
只有这个数据类型与String能够作为对象的键值。 并且它不会被自动转换成字符串,而是报错(只能.toString()或者.description)。
它跟 Ruby 的 Symbol 也是不一样的。尽管很像。
let id1 = Symbol("id");
let id2 = Symbol("id");//key可以留空(无参数)
alert(id1 == id2); // false,这是对象
看起来没什么用,但是如果你使用第三方的对象的时候,使用Symbol可以防止不同组建之间对对象的修改不会互相影响。也即,Symbol属性为“隐藏”属性。
如果在定义对象时使用Symbol,key值必须使用方括号。并且for.in不会遍历到Symbol。Object.keys(user) 也会忽略它们。但是Object.assign 会同时复制字符串和 symbol 属性。
如果你想要防止重复创建Symbol可以使用Symbol.for(key),在不存在此symbol时会创建一个,否则会返回已经创建的值。但是它依然不是Ruby。
Symbol.keyFor(sym)可以反向由符号查询值(仅限全局的Symbol)。上面代码框中的均不是全局量,直接查询会得到 undefined。因此此时只能用.toString()或者.description。
注:有一个内建方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 symbol。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 symbol。但大多数库、内建方法和语法结构都没有使用这些方法。
Primitive Value
JS中没有重载运算符。(这语言是怎么存活到现在的?)
三种类型转换(hint):String(各类字符串操作)、Number(各种数学操作,包括含大于小于的比较)、Default(二元加法、==等,通常除了Date对象都和"number"一致)。
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
- 调用
obj[Symbol.toPrimitive](hint)—— 带有 symbol 键Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是
"string"—— 尝试调用obj.toString()(优先) 或obj.valueOf(),无论哪个存在。 - 否则,如果 hint 是
"number"或"default"—— 尝试调用obj.valueOf()(优先) 或obj.toString(),无论哪个存在。obj[Symbol.toPrimitive] = function(hint) { // 这里是将此对象转换为原始值的代码 // 它必须返回一个原始值 // hint = "string"、"number" 或 "default" 中的一个 }如果
toString或valueOf返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。 默认情况下,普通对象具有toString和valueOf方法:toString方法返回一个字符串"[object Object]"。valueOf方法返回对象自身。
Attributes
对象属性除了value,还有三个标志:
writable— 如果为true,则值可以被修改,否则它是只可读的。enumerable— 如果为true,则会被在循环中列出(for.in),否则不会被列出。configurable— 如果为true,则此属性可以被删除,这些特性(列表中的三者)也可以被修改,否则不可以。let user = { name: "John" };
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');//查成分
alert( JSON.stringify(descriptor, null, 2 ) ); / 属性描述符: { "value": "John", "writable": true, "enumerable": true, "configurable": true } /
修改属性标志:使用`Object.defineProperty(obj, propertyName, descriptor)`。最后一个参数为“属性描述符”对象(即上文代码块中`getOwnPropertyDescriptor`返回的格式)。如果属性描述符里面没有某种flag则默认为`false`。当属性不存在时会使用`descriptor`创建参数,否则会更新其值。
一般默认的内置类型转换等方法的`enumerable`为`false`(无法使用`for.in`或`Object.keys()`),但是如果显性重新定义后则`enumerable`变为`True`。
一次修改多个:`Object.defineProperties(obj, { prop1: descriptor1, prop2: descriptor2 , ...});`
一次获得所有属性描述符:`Object.getOwnPropertyDescriptor(obj)`。
### Accessor
有点Python的`@property`内味了。
```javascript
let obj = {
get propName() {/* 当读取 obj.propName 时,getter 起作用*/},
set propName(value) {/* 当执行 obj.propName = value 操作时,setter 起作用 */}
};
相应地,访问器属性描述符没有writable,取而代之的是无参数函数get()和单参数函数set(value)。一个属性也要么是数据类型(有value)要么是访问器类型(get/set)。
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
set(value) {
[this.name, this.surname] = value.split(" ");
}
});
命名约定:以下划线开头的属性(例如_name)是内部属性,不应从外部访问,你可以使用get/set对其访问/赋值来操作。
代码更新时兼容旧属性的例子:(参见)
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
// 年龄是根据当前日期和生日计算得出的
Object.defineProperty(this, "age", { //构造函数内也可以直接这么写!
get() {
let todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
}
});
}
let john = new User("John", new Date(1992, 6, 1));
alert( john.birthday ); // birthday 是可访问的
alert( john.age ); // ……age 也是可访问的
Prototype Inheritance
你没有看错,在定义类之前我们就有原型和继承了。
原型使用隐藏属性[[Protytype]]实现。虽然你无法访问,但可以使用obj.__proto__来设置(它是这个隐藏属性的getter/setter)。在当前对象中找不到的属性会自动从原型中寻找。可以多层继承。但是原型只能有一个。
this属性永远是.前面的东西!!!继承仅仅影响方法,不会继承对象状态。
for.in会遍历继承的属性。obj.hasOwnProperty(key)返回是否是自己的属性(非继承)。笑点解析:这个函数本身就是继承的。
几乎其他所有的key/value获取方法都会忽略继承的属性(包括Object.keys/values/entries(obj)与delete等)。当然你重新赋值了一遍就相当于不是继承的了。
注意:绑定的不是某个名字,而是当时与名字绑定的对象。
如果想要用构造函数来绑定原型,可以给F.prototype属性赋值(不知道函数属性可以看[[#函数对象]]),之后使用new操作构造的对象的.__proto__就会与F.prototype共享一个对象。
默认的函数原型为F.prototype = { constructor: F };。因此,如果你想要保留constructor属性,你可以重新赋值或者只给prototype赋属性值。
关于原型的进一步思考:
我们知道默认的object.toString()返回的值为"[object Object]",但是它来自哪里?答案是来自系统内置的构造函数Object(),并且Object.prototype存储着属性toString()。
同理,其他对象(Array,Date,Function等等)都在prototype上面挂载了方法。而任意的arr/date/func.__proto__.__proto__ === Object.prototype。 问题来了,原始值(参见后文中[[#Primitive]])没有对象而只有对象**包装器**。也就是说访问原始值的属性/方法时,临时包装器会使用内置的构造器String, Number, Boolean等被创建。因此你可以通过String.prototype来给每个字符串来加个新的对象。(对其它类型同理,但是**最好不要这样做!**)你只能在 polyfilling 的时候这样做。不过还有另一种用法:方法借用。 当然,现在也不建议使用直接给proto`来设置和读取原型了(这一般只适用于浏览器,虽然对于服务器环境大多数也能用),建议使用:
- Object.getPrototypeOf(obj) —— 返回对象
obj的[[Prototype]]。 - Object.setPrototypeOf(obj, proto) —— 将对象
obj的[[Prototype]]设置为proto。__proto__不被反对的唯一的用法是在创建新对象时,将其用作属性:{ __proto__: ... }。 虽然,也有一种特殊的方法: - Object.create(proto, [descriptors]) —— 利用给定的
proto作为[[Prototype]]和可选的属性描述来创建一个空对象。(时刻记得属性描述器默认为false!) 于是我们得到了真正的对象克隆的完全体:let clone = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) );
Types
超级比较相等的方法:SameValueZero(x,y)参见ecma。
Primitive
原始类型也有方法(不包括null和undefined),但是为了尽可能保证原始的数据类型尽可能轻量,实现方法为“对象包装器”。注意对象包装器不能写入数据。
Number:均是符合IEEE-754规范的浮点数,/为直接相除(没有整除功能)。注意123456..toString(base) === (123456).toString(base)中第一个点会被解释为小数点。 经典.1+.2!=.3,这时舍入方法为Math.ceil/floor/round/trunc(num)或者num.toFixed(digits_after_dots)(注意它返回的是一个字符串,结果与round相同)。 为了防止精度消失,你可以将+6.35.toFixed(1)等转换为Math.round(6.35 * 10) / 10。 两个函数:isFinite(num)与isNaN(num),求值时会先转换为Number对象。 (不能使用==,因为NaN !== NaN!)有一个特殊的内建方法
Object.is,它类似于===一样对值进行比较,但它对于两种边缘情况更可靠:- 它适用于
NaN:Object.is(NaN, NaN) === true,这是件好事。 - 值
0和-0是不同的:Object.is(0, -0) === false,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。 在所有其他情况下,Object.is(a, b)与a === b相同。
- 它适用于
parseInt(str, [radix]), parseFloat(str, [radix])从第一个字符开始读取字符,直到无法被解析为数字为止,返回一个Number。可以读取带单位的值,例如"12px"。
-
Bigint:定义:const bigint = 1234n | Bigint(1234),除法变为取整。 不能直接与Number进行加减乘除,必须要强制类型转换;但是可以进行数值比较(2n > 1, 2n == 2, 2n !== 2)。转换为Number不能使用一元加法。 -
String:均是以UTF-16存储,反引号可以来定义多行字符串。
| --- | --- | \uXXXX |
以 UTF-16 编码的十六进制代码 XXXX 的 Unicode 字符,例如 \u00A9 —— 是版权符号 © 的 Unicode。它必须正好是 4 个十六进制数字。 |
\u{X…XXXXXX}(1 到 6 个十六进制字符) |
具有给定 UTF-32 编码的 Unicode 符号。一些罕见的字符用两个 Unicode 符号编码,占用 4 个字节。这样我们就可以插入长代码了。 |
|---|
查询长度:str.length;索引直接使用方括号(没有负数索引,必须得要查询常数),返回长度为1的字符串,找不到返回undefined;为不可变类型,不能改变索引处的值。
常见属性:str.toUpperCase(), str.toLowerCase();str.indexOf/lastIndexOf(substr, [pos])从pos开始查找,找不到返回-1,判断不是-1的技巧:按位非~;如果只需要查找是否存在可以str.includes/startsWith/endsWith(substr, [pos])(返回布尔值);str.split(char)/arr.join(char)。 |
方法 | 选择方式…… | 负值参数 |
|---|---|---|---|
slice(start[, end]) |
从 start 到 end(不含 end) |
允许 | |
substring(start[, end]) |
从 start 到 end(不含 end,若前比后大则交换) |
负值被视为 0 |
|
substr(start[, length]) |
从 start 开始获取长为 length 的字符串 |
允许 start 为负数 |
str.codePointAt(pos)与String.fromCodePoint(code)实现UTF码与字符的互相转换。
有时候为了恰当地比较一些有帽子的字符,可以使用str.localeCompare(str2),str靠前返回-1,靠后返回+1,相同返回0。(还可以加额外的参数来指定比较形式,参见docs);str.trim()可以删除字符串前后的空格。
Array:可以new Array()或者[]定义,性质和Python很相似。 依然不能使用负数索引,如果要使用得要array.at(-1)或者使用array.length(警告:这返回最大索引值加一,而且你甚至可以直接手动修改这个属性:增加时会多出一堆undefined,减小时数组会被截断,因此清空数组可以arr.length=0)。 你可以把JS中的数组看成一个deque,push/pop操作队尾,shift/unshift操作队头。push/shift可以一次加入多个元素,保持参数的顺序;pop/unshift无参数有返回值。 所有的数组都是对象,方括号值也与对象的用法相同,但是最好不要在任意索引处赋值/添加额外属性,否则程序优化会失效。 迭代可以使用for(;;)或者for.of(只返回value不返回key,对于类数组对象(类似Python中可迭代对象)有优化且只返回数字索引值,不建议用for.in)。 数组只有toString没有另外两个转换方式。 超级数组操作:arr.splice(start[, deleteCount, elem1, ..., elemN])从start(支持负数索引)删除deleteCount个元素并加入elem。 简单一些的修改:arr.slice([start][, end])(无参数时返回副本);arr.concat(arr1,...)链接数组或者元素;arr.forEach(function(item, index, array){})为依次执行(如果函数参数少于三个自动取前面的;函数返回值会被丢弃)。 查找:arr.indexOf(item[, from])—— 从索引from开始搜索item,如果找到则返回索引,否则返回-1;arr.includes(item[, from])则返回Boolean。判断方法均为===(但是方法includes可以正确的处理NaN,参见SameValue(x,y))。如果要找满足特定条件的元素,使用arr.find/filter(function(item, index, array){})(存在返回item/所有item的集合,不存在返回undefined)或者.findIndex/findLastIndex存在返回索引,不存在返回-1。 转换:arr.map(function(item, index, array){})来分别代入返回数组;arr.sort([fn])在原地址对数组进行排序(默认转化为字符串后按字典序排序,fn在大于/小于/等于时返回正/负/0,最终从小到大排序,比如arr.sort((a, b)=>a-b));arr.reverse()原位颠倒;arr.reduce/reduceRight(function(accumulator, item, index, array){}, [initial_sum])为整个数组返回一个值(从左到右/右到左)。Array是不能用typeof的(会返回object),但能够使用Array.isArray(val)。几乎所有调用函数的数组方法 —— 比如
find,filter,map,除了sort是一个特例,都接受一个可选的附加参数thisArg。thisArg参数的值在func中变为this。
Iterable
比方说Python中的range就是可迭代的(以及yield等等)。只有可迭代对象才能用for.of。
let range = {
from: 1,
to: 5
};
// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function() {
// ……它返回迭代器对象(iterator object):
// 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// 现在它可以运行了!
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}
for.of等价于以下显式调用迭代器的过程:
let str = "Hello";
// 和 for..of 做相同的事
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // 一个接一个地输出字符
}
- Iterable 是实现了
Symbol.iterator方法的对象。 - Array-like 是有索引和
length属性的对象,所以它们看起来很像数组。Array.from(obj[, mapFn, thisArg])可以接受可迭代对象或者类数组并得到一个真的数组(如果有mapFn可以额外映射一次)!这下终于可以处理双字节的字符了(UTF16):let str = '𝒳😂';
// 将 str 拆分为字符数组 let chars = Array.from(str);
alert(chars[0]); // 𝒳 alert(chars[1]); // 😂 alert(chars.length); // 2
-------------------
JS中还有 `Map`和`Set`(我嘞个STL啊)。两者都可以使用`for.of`迭代,顺序与插入顺序相同 。
`Map`可以使用任何类型的key。**不要使用方括号索引**,不然这个美好的特性就用不上了。你可以使用对象作为key(如果使用普通的方括号索引,两个字符串都会被`.toString()`变成`"[object Object]"`)。
- `new Map()` —— 创建 map,参数可以替换为一个两列的数组或其他可迭代对象。
- `map.set(key, value)` —— 根据键存储值,返回`map`本身,因此可以链式调用(叠加多个`.set(a, b)`)。
- `map.get(key)` —— 根据键来返回值,如果 `map` 中不存在对应的 `key`,则返回 `undefined`。
- `map.has(key)` —— 如果 `key` 存在则返回 `true`,否则返回 `false`。
- `map.delete(key)` —— 删除指定键的值。
- `map.clear()` —— 清空 map。
- `map.size` —— 返回当前元素个数。
- `map.keys()` —— 遍历并返回一个包含所有键的可迭代对象。迭代的顺序与插入值的顺序相同。
- `map.values()` —— 遍历并返回一个包含所有值的可迭代对象,
- `map.entries()` —— 遍历并返回一个包含所有实体 `[key, value]` 的可迭代对象,`for..of` 在默认情况下使用的就是这个。
- `map.forEach(function(value, key, map){...})`与`Array`的`forEach`相似。
如果我们想从一个已有的普通对象(plain object)来创建一个 `Map`,那么我们可以使用内建方法 [Object.entries(obj)](https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/entries),该方法返回对象的键/值对数组,该数组格式完全按照 `Map` 所需的格式。`Object.fromEntries` 方法的作用是相反的:给定一个具有 `[key, value]` 键值对的数组,它会根据给定数组创建一个对象。
`Set`相当于只有value没有key。
- `new Set(iterable)` —— 创建一个 `set`,如果提供了一个 `iterable` 对象(通常是数组),将会从数组里面复制值到 `set` 中。
- `set.add(value)` —— 添加一个值,返回 set 本身。重复添加同一个值不会改变`set`。
- `set.delete(value)` —— 删除值,如果 `value` 在这个方法调用的时候存在则返回 `true` ,否则返回 `false`。
- `set.has(value)` —— 如果 `value` 在 set 中,返回 `true`,否则返回 `false`。
- `set.clear()` —— 清空 set。
- `set.size` —— 返回元素个数。
- `set.keys()` —— 遍历并返回一个包含所有值的可迭代对象,
- `set.values()` —— 与 `set.keys()` 作用相同,这是为了兼容 `Map`,
- `set.entries()` —— 遍历并返回一个包含所有的实体 `[value, value]` 的可迭代对象,它的存在也是为了兼容 `Map`。
- `set.forEach(function(value, valueAgain, map){...})`——注意参数的变化。
还有一种查询key-value的方法:`Object.keys/values/entries(obj)`。三者均应该返回**数组**而不是上文中类似的可迭代对象(历史遗留问题)。
> `for.in`循环会忽略`Symbol`属性,`Object.keys/values/entries(obj)`也是。
> 如果你想要 `Symbol`类型的key,可以使用[Object.getOwnPropertySymbols](https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols)(仅包含Symbol的键)或者[Reflect.ownKeys(obj)](https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys)(返回 **所有** 键)。
------------
JS中更加神人的`WeakMap`和`WeakSet`:
`WeakMap`的key必须是对象而不能是原始值。并且,其中的作为Key的对象必须存在其他引用,否则会被垃圾回收!幽默弱引用。
```javascript
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 覆盖引用
// john 被从内存中删除了!
WeakMap 不支持迭代以及 keys(),values() 和 entries() 方法(因为内存回收是引擎决定何时执行的,因此这些方法的结果无法确定)。只有以下的方法:
weakMap.get(key)weakMap.set(key, value)weakMap.delete(key)weakMap.has(key)在多个组件时,WeakMap可以方便地实现缓存的功能。WeakSet同理,只能add, has, delete。不能迭代。
Generator
喜欢yield吗?那么你就要使用function*。
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "generator function" 创建了一个 "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]
赋值的时候,迭代器里的代码还没有真正运行。
当然每个迭代器都是可以调用.next()的,与前文相仿,里面会有value与done两个属性。
使用for.of循环时不会遍历最后的return值,你可以把它改成yield。
我们可以把上文中某个例子改成更加紧凑的形式:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*() 的简写形式
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1,2,3,4,5
套娃:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
yield* generateSequence(48, 57);// 0..9
yield* generateSequence(65, 90);// A..Z
yield* generateSequence(97, 122);// a..z
}
generator不是一个纯输出的结构,你当然可以给它整上输入(generator.next(input))。
function* gen() {
// 向外部代码传递一个问题并等待答案
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield 返回的 value
generator.next(4); // --> 将结果传递到 generator 中
你甚至还能给generator报错(generator.throw(err)):
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("The execution does not reach here, because the exception is thrown above");
} catch(e) {
alert(e); // 显示这个 error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("The answer is not found in my database")); // (2)
如果generator没有接住这个error,它会从函数中掉出来,你可以在generator.throw外面在套上一层try.catch来解决。
generator.return(value)可以使当前迭代器变成{value, done: true}。
与async一起的用法参见后文。
Assignment
Python:这还用学?
let [firstName, surname] = arr;
//let firstName = arr[0], surname = arr[1];
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert( title ); // Consul
let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]); //这两者都是迭代器发力了
let user = {
name: "John",
age: 30
};
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:John, then age:30
}
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// rest 是包含从第三项开始的其余数组项的数组
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
let [firstName, surname] = [];
alert(firstName); // undefined
alert(surname); // undefined
// 只会提示输入姓氏,能够被赋值的对象的缺省值会被忽略
let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"];
alert(name); // Julius(来自数组)
alert(surname); // 你输入的值
let {height, width, title} = { title: "Menu", height: 200, width: 100 }
alert(title); // Menu
alert(width); // 100
alert(height); // 200
let options = {
title: "Menu",
width: 100,
height: 200
};
// { sourceProperty: targetVariable }
let {width: w /*=prompt("width? ") 在能够赋值时被忽略*/, height: h, title} = options; //这个语法可能和直觉相反
let {title, ...rest} = options;
// 现在 title="Menu", rest={height: 200, width: 100}
注意:在不使用
let时,引擎可能会把大括号认为是代码块;为了避免这种情况,要在语句的两端加上()。 以上这些代码可以嵌套(例子略)。
智能传参:感觉不如Python。(注意,调用函数的时候参数不能留空,而数组可以。)
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
function showMenu({
title = "Untitled",
width: w = 100, // width goes to w
height: h = 200, // height goes to h
items: [item1, item2] // items first element goes to item1, second to item2
} = {}) { //You can use showMenu() instead of showMenu({})
alert( `${title} ${w} ${h}` ); // My Menu 100 200
alert( item1 ); // Item1
alert( item2 ); // Item2
}
showMenu(options);
JSON
对象转换为JSON:JSON.stringify(object);JSON转换为对象:JSON.parse(json)。
注意:JSON中所有Key被改写为有双引号的字符串。
JSON支持的类型:Object Array String Number Boolean null
JSON不包含:函数/方法、Symbol/undefined的key/value。不能存在循环引用。
完整版:JSON.stringify(value[, replacer, space]),replacer代表要编码的属性数组或者映射函数,space表示格式化所需要的空格数量。
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}
alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
"title":"Conference",
"participants":[{"name":"John"},{"name":"Alice"}],
"place":{"number":23}
}
alert( JSON.stringify(meetup, function replacer(key, value) {
alert(`${key}: ${value}`);
return (key == 'occupiedBy') ? undefined : value;
}));
/* key:value pairs that come to replacer:
: [object Object]
title: Conference
participants: [object Object],[object Object]
0: [object Object]
name: John
1: [object Object]
name: Alice
place: [object Object]
number: 23
occupiedBy: [object Object]
*/
可以给对象自定义toJSON()方法。
JSON.parse(str, [reviver])的可选参数对每个(key, value)调用并转换,返回value。
注意:以上所用方法对于嵌套的元素均会起效果!
Function
可变参数
JS中原则上你可以传入任意个数的参数,当形参对于实参时多出部分变成undefined,反之则会忽略多余的实参。
如果想要收集多余参数,可以使用...args(可以使用其他名称)。一定要把它放在最后一位。args会成为一个数组。
有一个名为 arguments 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。箭头函数没有arguments,与this类似。(它访问的arguments相当于外部函数的值。)
如果想要将数组/可迭代对象(不能直接用于类数组,必须得要Array.from())进行解包操作,依然可以在参数中写成...arr(此时当然不一定放在最后)。可以发现如果将...替换为*,以上的两个操作将会变为我们在Python中熟悉的形式。
因此,我们又获得了浅拷贝的另一种形式[...arr],{...obj}。
一个高级的柯里化实现:
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
作用域
let与const均为块作用域,无声明提升。JS中存在闭包。每次调用函数都会产生一个全新的词法环境。这块跟Python几乎一模一样(把所谓Lexical enviroment替换为Frame就一样了)。参见这一章的内容(强烈建议把习题都做一遍)。
JS中的全局对象为globalThis。在浏览器中,它叫做window;在Node.js中它叫做global,在别的环境中可能会有别名。
全局对象的属性/方法可以直接调用,省略globalThis.;使用var声明的全局变量会变为全局对象的属性(不要这么做!)。全局函数声明也会有同样的效果(函数表达式、箭头函数都不是函数声明)。你也可以直接编辑全局变量的某些属性,来等效地让它变为全局的。
函数对象
函数的名字可以使用func.name(没有括号!)来访问,即使在创建时使用函数表达式赋值依然能够正确地识别(上下文命名)。如果上下文命名失败,name=""。func.length返回形参个数(不包括可变参数)。
在函数内部也可以定义属性(请与变量区分)。你可以用它来部分地代替闭包(如果你想要某个参数能被外部代码修改那就使用函数属性,否则使用闭包)。
function sayHi() {
alert("Hi");
// 计算调用次数
sayHi.counter++;
}
sayHi.counter = 0; // 初始值
sayHi(); // Hi
sayHi(); // Hi
alert( `Called ${sayHi.counter} times` ); // Called 2 times
对于命名的函数表达式(尽管有名字但依旧不是函数声明)有两个特性:在函数体内能够递归调用;在函数外其名称不可见。
正如“函数对象”其名,函数也可以使用new来构造:
let func = new Function ([arg1, arg2, ...argN], functionBody);
其中的所有参数均为字符串。
let sum = new Function('a', 'b', 'return a + b');
//let sum = new Function('a , b', 'return a+b');
alert( sum(1, 2) ); // 3
这种情况常见于要从服务器接受代码来动态编译函数。注意此时得到的函数的词法环境为全局变量!
装饰器种种
并没有美丽的@语法,你只能直接调用来函数装饰。当装饰到含有this的函数时,可以使用func.call(context, ...args)方法,第一个参数是为this:
func(1, 2, 3); //this == globalThis
func.call(obj, 1, 2, 3); //this == obj
/*
function hash() {
return [].join.call(arguments); //方法借用
}
*/
如果你不想传入参数列表而是一个类数组,那么将call变为apply即可。
如果你只想要得到一个绑定了this的函数,请使用func.bind(context)(一个函数只能被绑定一次)。bind结束后得到的是另一个对象,它不会保留原来加上的函数属性。
let bound = func.bind(context, [arg1], [arg2], ...);
是的,完整版的func.bind还可以绑定参数。这样比使用闭包来调用方便多了。但是如果你只想要绑定参数,你确实可以自定义一个东西:
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
=>
箭头函数没有this,没有arguments,它全部从外界获取。箭头函数不会离开当前的上下文。
同理,箭头函数不能作为构造器,不能将其用到new后面。它也没有super。
Class
基础
笑点解析:继承都讲完了才出现类。
class MyClass {
// class 方法
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
let class = new MyClass();
alert(typeof User); // function
类方法之间没有括号!
正如上文,类是一种函数(令人忍俊不禁)。也就是说,MyClass === MyClass.prototype.constructor;上面的constructor与每个method为MyClass.prototype的方法。(也就是说,每个创建的对象相当于继承了类。)
那么,为什么我不直接使用普通的构造函数呢?通过 class 创建的函数具有特殊的内部属性标记 [[IsClassConstructor]]: true。因此,它与手动创建并不完全相同。例如,与普通函数不同,必须使用 new 来调用它;类有自己的toString;类方法不可枚举(不能使用for.in);等等。
当然类也有类表达式之说,类表达式也可以有自己的姓名。
// “命名类表达式(Named Class Expression)”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
let User = class MyClass {
sayHi() {
alert(MyClass); // MyClass 这个名字仅在类内部可见
}
};
new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容
alert(MyClass); // error,MyClass 在外部不可见
同理,类也可以使用getter.setter等等,或者使用计算属性(中括号内部的东西)。 所谓类字段,就是在定义类的时候加入属性(这个在C/C++看起来这么显而易见的东西居然在最近才放入标准,可能需要polyfill)。
class User {
name = "John"; //孩子们很多旧版本都不支持我
//你甚至可以使用 name = prompt("Name, please?", "John") 这种语句。
sayHi() {
alert(`Hello, ${this.name}!`);
}
}
new User().sayHi(); // Hello, John!
所有类字段会挂在实例对象而非原型上。
回忆:在某些时候使用类方法做其它函数的参数会出现this丢失的问题:
class Button {
constructor(value) {
this.value = value;
}
click() {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // undefined
这时候有三种方法:
- 传递一个包装函数,例如
setTimeout(() => button.click(), 1000)。 - 将方法绑定到对象(
func.bind),例如在 constructor 中。 - 在类中写下
click = () => { alert(this.value) }。这对于事件监听非常有用。
继承
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
let animal = new Animal("My animal");
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!
此时 Rabbit.prototype.[[prototype]] === Animal.prototype。
注意,extends之后可以写任意表达式:
function f(phrase) {
return class {
sayHi() { alert(phrase); }
};
}
class User extends f("Hello") {}
new User().sayHi(); // Hello
找爹:使用super关键字来更加灵活地重写部分属性。(箭头函数依然没有super,只会从外部找。)
- 执行
super.method(...)来调用一个父类方法。 -
执行
super(...)来调用一个父类 constructor(只能在我们的 constructor 中)。class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stands still.`); }
}
class Rabbit extends Animal {
hide() {
alert(${this.name} hides!);
}
stop() { super.stop(); // 调用父类的 stop this.hide(); // 然后 hide } }
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // White Rabbit stands still. White Rabbit hides!
哦天呐,我们还没有重写构造器呢!默认情况下如果没写构造器那就和`super`产生一样的行为。让我们来试试:
```javascript
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
this.speed = 0;
this.name = name;
this.earLength = earLength;
}
// ...
}
// 不工作!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
为什么报错:一句话:继承类的constructor必须调用super(...),并且一定要在使用this之前调用!
为什么会这样?因为继承类的构造函数会存在特殊的隐藏标签[[Constructorkind]]:"derived"。因此它在new的时候行为会发生改变:
- 当通过
new执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给this。 - 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。
所以才会出现上文中的那些行为。正确写法应该是把上文中
constructor的前两行改为super(name)。 更加诡异的是对于类字段的继承。父类构造器总会使用自己的字段值,而非子类重写的那一个。然而类方法使用的是子类的值。(例子参见此处) 问题来了,这是为什么呢?原因在于字段初始化的顺序。类字段是这样初始化的: - 对于基类(还未继承任何东西的那种),在构造函数调用前初始化。
- 对于派生类,在
super()后立刻初始化。 所以,字段(常规属性)与方法的区别就在于此。后文会详细介绍super究竟是怎样一个屎山。
关于super的一切
你可能觉得调用super的方法只需要将其替换为this.__proto__就可以了:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
eat() {
// ...bounce around rabbit-style and call parent (animal) method
this.__proto__.eat.call(this); // (*)
}
};
let longEar = {
__proto__: rabbit,
eat() {
// ...do something with long ears and call parent (rabbit) method
this.__proto__.eat.call(this); // (**)
}
};
longEar.eat(); // Error: Maximum call stack size exceeded
Oops,死循环。为了解决这种野蛮的错误,JS添加了特殊内部属性[[HomeObject]]。一个类/对象的方法的[[HomeObject]]就是该类/对象。于是调用super的时候就会从它的[[HomeObject]]中寻找。
很遗憾,[[HomeObject]]是永久绑定的,无法被更改。
let animal = {
sayHi() {
alert(`I'm an animal`);
}
};
// rabbit 继承自 animal
let rabbit = {
__proto__: animal,
sayHi() {
super.sayHi();
}
};
let plant = {
sayHi() {
alert("I'm a plant");
}
};
// tree 继承自 plant
let tree = {
__proto__: plant,
sayHi: rabbit.sayHi // (*)
};
tree.sayHi(); // I'm an animal (?!?)
这时候方法借用时,super无法动态绑定。这使我们更加清晰地看出:
方法,不是函数属性!!!(方法有[[HomeObject]]而函数没有。)
所以,不要借用有super的函数。
static
想你了C++。static标签中调用的this就是类构造器。
static方法都是挂载在Class而非Class.Prototype。
通常,静态方法用于实现属于整个类,但不属于该类任何特定对象的函数。
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static compare(articleA, articleB) {
return articleA.date - articleB.date;
}
}
// 用法
let articles = [
new Article("HTML", new Date(2019, 1, 1)),
new Article("CSS", new Date(2019, 0, 1)),
new Article("JavaScript", new Date(2019, 11, 1))
];
articles.sort(Article.compare);
alert( articles[0].title ); // CSS
另一个例子:
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static createTodays() {
// 记住 this = Article
return new this("Today's digest", new Date());
}
}
let article = Article.createTodays();
alert( article.title ); // Today's digest
注意:静态方法不适用于单个对象。 当然你可以使用静态的类属性(很遗憾这依然是一个最近新增的feature)。静态属性可以继承(当然是在类与类之间而不是方法与方法之间)。
public, private, protected
没有这三个东西
没有这三个保留字,但是你可以某种程度上地实现他们。
public:不需要实现。
protected:没有语法层面的强制实现。我们一般约定单下划线的开头的类/对象属性/方法只能内部访问,读写操作使用对象方法或get/set实现。很显然这是可以继承的。
private:最近新添的feature允许私有属性与方法以#开头。调用的时候依然需要加上#,因此它们不会与其它属性字段发生冲突。它们不能使用obj.#a或obj["#a"]等方式来访问。大部分情况下你只能使用polyfills实现。
内建的许多类(Array, Map等)都是可以扩展的。
// 给 PowerArray 新增了一个方法(可以增加更多)
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}
}
let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false
let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false
alert(arr.constructor === PowerArray); // true
我们甚至可以给这个类添加一个特殊的静态 getter Symbol.species,它会返回 JavaScript 在内部用来在 map 和 filter 等方法中创建新实体的 constructor。
如果我们希望像 map 或 filter 这样的内建方法返回常规数组,我们可以在 Symbol.species 中返回 Array,就像这样:
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}
// 内建方法将使用这个作为 constructor
static get [Symbol.species]() {
return Array;
}
}
let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false
// filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
let filteredArr = arr.filter(item => item >= 10);
// filteredArr 不是 PowerArray,而是 Array
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
注:内建类不会继承静态方法。因为他们之间不是
extends的关系;作为构造函数,它们之间只有.prototype存在继承关系([[Prototype]]);而它们本身之间不存在[[Prototype]]的关系。
类型检查
省流:
| 用于 | 返回值 | |
|---|---|---|
typeof |
原始数据类型 | string |
{}.toString |
原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象 |
string |
instanceof |
对象 | true/false |
obj instanceof Class // 返回obj是否为Class或者其衍生类
执行逻辑:
- 如果这儿有静态方法
Symbol.hasInstance,那就直接调用这个方法: - 大多数 class 没有
Symbol.hasInstance。在这种情况下,标准的逻辑是:使用obj instanceOf Class检查Class.prototype是否等于obj的原型链(不停叠加.__proto__)中的原型之一。Mixin
JS没有多继承,所以就有了mixin,它是一个无需继承方法就能被其他类使用的类。
// mixin let sayHiMixin = { sayHi() { alert(`Hello ${this.name}`); }, sayBye() { alert(`Bye ${this.name}`); } };
// 用法: class User { constructor(name) { this.name = name; } }
// 拷贝方法 Object.assign(User.prototype, sayHiMixin);
// 现在 User 可以打招呼了 new User("Dude").sayHi(); // Hello Dude!
## Error
经典try catch throw:
```javascript
try {
//...
} catch (err) {
//...
} // finally {} optional.无论如何都会执行
error的属性:name、message、stack(当前调用栈),等等。
抛出错误使用throw,后面可以接任何东西,但是最好使用含有name和message属性的对象。
内建error对象:Error,SyntaxError,ReferenceError,TypeError 等。它们的构造器均接受(msg)的参量。
可以在catch块中加入各种instanceof来判断错误类型,甚至可以再次throw错误。
注意:即使try中有return,finally中的内容依然会执行。
Async
回调
JS中的代码异步调用,所以在执行下文的操作时上文可能还并没有执行完成。 有一个很简单的解决方案(这里面用了一些DOM方法):
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script); //callback(err, script)传入你想要在加载之后执行的函数
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
但是如果要多层嵌套的话就炸了,怎么办?答案是使用Promise对象。
let promise = new Promise(function(resolve, reject) {
// 当 promise 被构造完成时,自动执行此函数
// 1 秒后发出工作已经被完成的信号,并带有结果 "done"
setTimeout(() => resolve("done"), 1000);
});
当 executor 获得了结果(以下两者只能最终出现一个,之后的都会被忽略):
resolve(value)—— 如果任务成功完成并带有结果value。reject(error)—— 如果出现了 error,error即为 error 对象。 内部属性(你无法直接访问,只能通过类方法):state—— 最初是"pending",然后在resolve被调用时变为"fulfilled",或者在reject被调用时变为"rejected"。result—— 最初是undefined,然后在resolve(value)被调用时变为value,或者在reject(error)被调用时变为error。 处理得到的内容:promise.then( function(result) { /* handle a successful result */ }, function(error) { /* handle an error */ } ); // 如果只想要result,第二个参数可以不填
// .catch(f) 与 promise.then(null, f) 一样 promise.catch(alert); // 1 秒后显示 "Error: Whoops!"
还有`promise.finally(f)`的方法,它类似于`.then(f, f)`,区别:
- `finally` 处理程序(handler)没有参数。在 `finally` 中,我们不知道 promise 是否成功。这没关系,因为我们的任务通常是执行“常规”的完成程序(finalizing procedures)。
- `finally` 处理程序将结果或 error “传递”给下一个合适的处理程序。
例如,在这结果被从 `finally` 传递给了 `then`:
```javascript
new Promise((resolve, reject) => {
setTimeout(() => resolve("value"), 2000)
})
.finally(() => alert("Promise ready")) // 先触发
.then(result => alert(result)); // <-- .then 显示 "value"
总结:
finally处理程序没有得到前一个处理程序的结果(它没有参数)。而这个结果被传递给了下一个合适的处理程序。- 如果
finally处理程序返回了一些内容,那么这些内容会被忽略。 -
当
finally抛出 error 时,执行将转到最近的 error 的处理程序。 多个Promise可以形成链式结构。你可以加上无数个.then,每一个接受上一个的返回值(或者Promise,如果这样其它的程序会等待它settled——即fulfilled或rejected——之后调用)。这和多个分立的
promise.then(result)结构不同,后者产生的是树形结构。new Promise(function(resolve, reject) {setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // () setTimeout(() => resolve(result 2), 1000); });
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => { setTimeout(() => resolve(result * 2), 1000); });
}).then(function(result) {
alert(result); // 4
});
**如果 `.then`(或 `catch/finally` 都可以)处理程序返回一个 promise,那么链的其余部分将会等待,直到它状态变为 settled。当它被 settled 后,其 result(或 error)将被进一步传递下去。**