书名 | JavaScript 高级程序设计 |
---|---|
作者 | [美] 马特·弗里斯比 |
状态 | 阅读中 |
根据遗忘曲线:如果没有记录和回顾,6天后便会忘记75%的内容
读书笔记正是帮助你记录和回顾的工具,不必拘泥于形式,其核心是:记录、翻看、思考
为了方便操作原始值,ECMAScript 提供了 3 种特殊的引用类型:Boolean、Number 和 String。
每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。
比方说看下面这个例子:
let s1 = "some text";
let s2 = s1.substring(2);
在这里,s1 是一个包含字符串的变量,它是一个原始值。第二行紧接着在 s1 上调用了 substring()
方法,并把结果保存在 s2 中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。
具体来说,当第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下 3 步:
类似
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;
let s1 = "some text";
s1.color = "red";
console.log(s1.color); // undefined
这里的第二行代码尝试给字符串 s1 添加了一个 color 属性。可是,第三行代码访问 color 属性时,它却不见了。原因就是第二行代码运行时会临时创建一个 String 对象,而当第三行代码执行时,这个对象已经被销毁了。实际上,第三行代码在这里创建了自己的 String 对象,但这个对象没有 color 属性。(有种阅后即焚的感觉,看完一行代码就烧掉)
用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回
null。返回的数组虽然是 Array 的实例,但包含两个额外的属性:index 和 input。index 是字符串
中匹配模式的起始位置,input 是要查找的字符串。这个数组的第一个元素是匹配整个模式的字符串,
其他元素是与表达式中的捕获组匹配的字符串。如果模式中没有捕获组,则数组只包含一个元素。
返回 true,否则返回 false。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。
test()经常用在 if 语句中:
RegExp 构造函数本身也有几个属性。(在其他语言中,这种属性被称为静态属性。)这些属性适用于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。这些属性还有一个特点,就是可以通过两种不同的方式访问它们。换句话说,每个属性都有一个全名和一个简写。下表列出了RegExp 构造函数的属性。
通过这些属性可以提取出与 exec()和 test()执行的操作相关的信息。来看下面的例子:
let text = 'this has been a short summer';
let pattern = /(.)hort/g;
if (pattern.test(text)) {
console.log(RegExp.input); // this has been a short summer
console.log(RegExp.leftContext); // this has been a
console.log(RegExp.rightContext); // summer
console.log(RegExp.lastMatch); // short
console.log(RegExp.lastParen); // s
}
RegExp
还有其他几个构造函数属性,可以存储最多 9 个捕获组的匹配项。这些属性通过 RegExp.$1~RegExp.$9
来访问,分别包含第 1~9 个捕获组的匹配项。在调用 exec()
或test()
时,这些属性就会被填充,然后就可以像下面这样使用它们:
let someDate = new Date(Date.parse("May 23, 2019"));
let someDate = new Date("May 23, 2019");
// GMT 时间 2000 年 1 月 1 日零点
let y2k = new Date(Date.UTC(2000, 0));
// GMT 时间 2005 年 5 月 5 日下午 5 点 55 分 55 秒
let allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55));
- [UTC 与 GMT 的区别](https://www.yuque.com/tully/efwkni/xqf8dmpqbqngmpgh)
// 起始时间
let start = Date.now();
// 调用函数
doSomething();
// 结束时间
let stop = Date.now(),
result = stop - start;
let y2k1 = new Date('2000-01-01T00:00:00').toLocaleString();
let y2k2 = new Date('2000-01-01T00:00:00').toString();
let y2k3 = new Date('2000-01-01T00:00:00').valueOf();
console.log(y2k1); // 1/1/2000, 12:00:00 AM
console.log(y2k2); // Sat Jan 01 2000 00:00:00 GMT+0800 (China Standard Time)
console.log(y2k3); // 946656000000
Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串:
console.log(new Date().toDateString());
console.log(new Date().toTimeString());
console.log(new Date().toLocaleDateString());
console.log(new Date().toLocaleTimeString());
console.log(new Date().toUTCString());
console.log(new Date().toString());
这些方法的输出与 toLocaleString()和 toString()一样,会因浏览器而异。因此不能用于在 用户界面上一致地显示日期
var color = "blue";
function getColor() {
let color = "red";
{
let color = "green";
return color;
}
}
console.log(getColor());
function problem() {
let objectA = new Object();
let objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异, 但基本上都是根据已分配对象的大小和数量来判断的。比如,根据 V8 团队 2016 年的一篇博文的说法: “在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃 圾回收。”
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article();
a2.author = 'Jake';
// 定时器也可能会悄悄地导致内存泄漏。
let name = 'Jake';
setInterval(() => {
console.log(name);
}, 100);
// 使用 JavaScript 闭包很容易在不知不觉间造成内存泄漏。请看下面的例子:
let outer = function() {
let name = 'Jake';
return function() {
return name;
};
};
// 调用 outer()会导致分配给 name 的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回
// 的函数存在就不能清理 name,因为闭包一直在引用着它。假如 name 的内容很大(不止是一个小字符
// 串),那可能就是个大问题了。
很多开发者错误地认为, 当在局部作用域中修改对象而变化反映到全局时,就意味着参数是按引用传递的。为证明对象是按值传 递的,我们再来看看下面这个修改后的例子
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name);
如果 person 是按引用传递的,那么 person 应该自动将 指针改为指向 name 为"Greg"的对象。可是,当我们再次访问 person.name 时,它的值是"Nicholas", 这表明函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指 向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的
重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有 一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味 着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函 数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想
比方说,Object => Array => [1, 2, 3]
提供了拦截并向基本操作嵌入额外行为的能力
代理是目标对象的抽象 ,从很多方面看,代理类似 C++指针,因为它可以 用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。 但直接操作会绕过代理施予的行为
Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。
const target = {
foo: "bar",
};
const handler = {
get() {
return "intercepted";
},
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // TypeError
const o = {};
if(Reflect.defineProperty(o, 'foo', {value: 'bar'})) {
console.log('success');
} else {
console.log('failure');
}
Reflect.get() // 可以替代对象属性访问操作符。
Reflect.set() // 可以替代=赋值操作符。
Reflect.has() // 可以替代 in 操作符或 with()。
Reflect.deleteProperty() // 可以替代 delete 操作符。
Reflect.construct() // 可以替代 new 操作符
const fn = async () => {
console.log(1);
// return "is Promise?";
};
console.log(fn());
JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息 队列中推送一个任务,这个任务会恢复异步函数的执行
async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。 毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别
要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰 到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息 队列中推送一个任务,这个任务会恢复异步函数的执行
async function foo() {
console.log(await Promise.resolve("foo"));
}
async function bar() {
console.log(await "bar");
}
async function baz() {
console.log("baz");
}
foo();
bar();
baz();
// baz
// foo
// bar
async function foo() {
console.log(await Promise.resolve("2"));
console.log(3);
setTimeout(console.log, 0, 6);
}
async function bar() {
console.log(await "4");
console.log(5);
setTimeout(console.log, 0, 7);
}
async function baz() {
console.log("1");
}
foo();
bar();
baz();
// baz
// foo
// bar
因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。下面的例子演 示了这一点:
async function foo() {
console.log(2);
await console.log(2.5);
setTimeout(console.log, 0, 6);
console.log(4);
await console.log(4.5);
setTimeout(console.log, 0, 7);
console.log(5);
}
console.log(1);
foo();
console.log(3);
function sleep(delay = 1000) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
const fn = async () => {
const t0 = new Date();
await sleep();
console.log(new Date() - t0);
};
fn();
如果使用 await 时不留心,则很可能错过平行加速的机会。来看下面的例子,其中顺序等待了 5 个随机的超时:
async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve();
}, delay));
}
async function foo() {
const t0 = Date.now();
await randomDelay(0);
await randomDelay(1);
await randomDelay(2);
await randomDelay(3);
await randomDelay(4);
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 877ms elapsed
加速的程序
async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) =>
setTimeout(() => {
setTimeout(console.log, 0, `${id} finished`);
resolve();
}, delay)
);
}
async function foo() {
const t0 = Date.now();
const p0 = randomDelay(0);
const p1 = randomDelay(1);
const p2 = randomDelay(2);
const p3 = randomDelay(3);
const p4 = randomDelay(4);
await p0;
await p1;
await p2;
await p3;
await p4;
setTimeout(console.log, 0, `${Date.now() - t0}ms elapsed`);
}
foo();
用 for 改写
async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) =>
setTimeout(() => {
console.log(`${id} finished`);
resolve(id);
}, delay)
);
}
async function foo() {
const t0 = Date.now();
const promises = Array(5)
.fill(null)
.map((_, i) => randomDelay(i));
for (const p of promises) {
console.log(`awaited ${await p}`);
}
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
如何串行执行期约并把值传给后续的期约。使用 async/await,期约连锁会变 得很简单
async function addTwo(x) {
return x + 2;
}
async function addThree(x) {
return x + 3;
}
async function addFive(x) {
return x + 5;
}
async function addTen(x) {
for (const fn of [addTwo, addThree, addFive]) {
console.log(await fn(x));
x = await fn(x);
// console.log(x);
// await fn(x) 是结果,不是 Promise
}
return x;
}
addTen(9).then(console.log); // 19
到目前为止,我们讨论期约连锁一直围绕期约的串行执行,忽略了期约的另一个主要特性:异步产生值并将其传给处理程序。基于后续期约使用之前期约的返回值来串联期约是期约的基本功能。这很像 函数合成,即将多个函数合成为一个函数,比如:
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
return addFive(addTwo(addThree(x)));
}
console.log(addTen(7)); // 17
在这个例子中,有 3 个函数基于一个值合成为一个函数。类似地,期约也可以像这样合成起来,渐 进地消费一个值,并返回一个结果
function addTwo(x) {
return x + 2;
}
function addThree(x) {
return x + 3;
}
function addFive(x) {
return x + 5;
}
function addTen(x) {
return Promise.resolve(x).then(addTwo).then(addThree).then(addFive);
}
addTen(8).then(console.log); // 18
使用 Array.prototype.reduce()可以写成更简洁的形式
function addTwo(x) {
return x + 2;
}
function addThree(x) {
return x + 3;
}
function addFive(x) {
return x + 5;
}
function addTen(x) {
return [addTwo, addThree, addFive].reduce(
(promise, fn) => promise.then(fn),
Promise.resolve(x)
);
}
addTen(8).then(console.log); // 18
到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理 程序。拿到返回值后,就可以进一步对这个值进行操作。比如,第一次网络请求返回的 JSON 是发送第 二次请求必需的数据,那么第一次请求返回的值就应该传给 onResolved 处理程序继续处理。当然,失 败的网络请求也应该把 HTTP 状态码传给 onRejected 处理程序。
在执行函数中,解决的值和拒绝的理由是分别作为 resolve()和 reject()的第一个参数往后传的。然后,这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一参数。下面的例子展示了上述传递过程
let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value) => console.log(value)); // foo
let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar
拒绝期约类似于 throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。
let p1 = new Promise((resolve, reject) => reject(Error('foo')));
let p2 = new Promise((resolve, reject) => { throw Error('foo'); });
let p3 = Promise.resolve().then(() => { throw Error('foo'); });
let p4 = Promise.reject(Error('foo'));
setTimeout(console.log, 0, p1); // Promise : Error: foo
setTimeout(console.log, 0, p2); // Promise : Error: foo
setTimeout(console.log, 0, p3); // Promise : Error: foo
setTimeout(console.log, 0, p4); // Promise : Error: foo
// 也会抛出 4 个未捕获错误
期约可以以任何理由拒绝,包括 undefined,但最好统一使用错误对象。这样做主要是因为创建 错误对象可以让浏览器捕获错误对象中的栈追踪信息,而这些信息对调试是非常关键的。例如,前面例 子中抛出的 4 个错误的栈追踪信息如下:
Uncaught (in promise) Error: foo
at Promise (test.html:5)
at new Promise (<anonymous>)
at test.html:5
Uncaught (in promise) Error: foo
at Promise (test.html:6)
at new Promise (<anonymous>)
at test.html:6
Uncaught (in promise) Error: foo
at test.html:8
Uncaught (in promise) Error: foo
at Promise.resolve.then (test.html:7)
这个例子同样揭示了异步错误有意思的副作用。正常情况下,在通过 throw()关键字抛出错误时, JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令:
throw Error('foo');
console.log('bar'); // 这一行不会执行
// Uncaught Error: foo
但是,在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时 继续执行同步指令
Promise.reject(Error('foo'));
console.log('bar');
// bar
// Uncaught (in promise) Error: foo
这不包括捕获执行函数中的错误,在解决或拒绝期约之前,仍然可以使用 try/catch 在执行函数 中捕获错误:
let p = new Promise((resolve, reject) => {
try {
console.log(1);
throw Error("foo");
console.log(2);
} catch (e) {
console.log('error:>>', e);
}
resolve("bar");
});
setTimeout(console.log, 0, p);
// ?
then()和 catch()的 onRejected 处理程序在语义上相当于 try/catch。出发点都是捕获错误之 后将其隔离,同时不影响正常逻辑执行。
为此,onRejected 处理程序的任务应该是在捕获异步错误之 后返回一个解决的期约。下面的例子中对比了同步错误处理与异步错误处理:
// 同步
console.log('begin synchronous execution');
try {
throw Error('foo');
} catch(e) {
console.log('caught error', e);
}
console.log('continue synchronous execution');
// begin synchronous execution
// caught error Error: foo
// continue synchronous execution
// 异步的两个例子
new Promise((resolve, reject) => {
console.log("begin asynchronous execution");
reject(Error("bar"));
})
.catch((e) => {
console.log("caught error", e);
})
.then(() => {
console.log("continue asynchronous execution");
});
new Promise((resolve, reject) => {
console.log("begin asynchronous execution");
reject(Error("bar"));
})
.then(() => {
console.log("continue asynchronous execution");
})
.catch((e) => {
console.log("caught error", e);
});
把期约逐个地串联起来是一种非常有用的编程模式。之所以可以这样做,是因为每个期约实例的方 法(then()、catch()和 finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。比如
let p = new Promise((resolve, reject) => {
console.log('first');
resolve();
});
p.then(() => console.log('second'))
.then(() => console.log('third'))
.then(() => console.log('fourth'));
// first
// second
// third
// fourth
这个实现最终执行了一连串同步任务。正因为如此,这种方式执行的任务没有那么有用,毕竟分别 使用 4 个同步函数也可以做到:
(() => console.log('first'))();
(() => console.log('second'))();
(() => console.log('third'))();
(() => console.log('fourth'))();
要真正执行异步任务,可以改写前面的例子,让每个执行器都返回一个期约实例。这样就可以让每个后续期约都等待之前的期约,也就是串行化异步任务。
比如,可以像下面这样让每个期约在一定时间后解决:
let p1 = new Promise((resolve, reject) => {
console.log("p1 打开网页");
setTimeout(resolve, 1000);
});
p1.then(
() =>
new Promise((resolve, reject) => {
console.log("p2 请求登录接口,登录中ing");
setTimeout(resolve, 2000);
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log("p3 加载图片中ing");
setTimeout(resolve, 3000);
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log("p4 加载文字");
setTimeout(resolve, 1000);
})
);
把生成期约的代码提取到一个工厂函数中,就可以写成这样
function delayedResolve(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
});
}
delayedResolve("p1 executor")
.then(() => delayedResolve("p2 executor"))
.then(() => delayedResolve("p3 executor"))
.then(() => delayedResolve("p4 executor"));
每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。这种结构可以简 洁地将异步任务串行化,解决之前依赖回调的难题。假如这种情况下不使用期约,那么前面的代码可能 就要这样写了:
function delayedExecute(str, callback = null) {
setTimeout(() => {
console.log(str);
callback && callback();
}, 1000);
}
delayedExecute("p1 callback", () => {
delayedExecute("p2 callback", () => {
delayedExecute("p3 callback", () => {
delayedExecute("p4 callback");
});
});
});
因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个 期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等 待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序
下面的例子展示了一种期约有向图,也就是二叉树:
// A
// / \
// B C
// /\ /\
// D E F G
let A = new Promise((resolve, reject) => {
console.log("A");
resolve();
});
let B = A.then(() => console.log("B"));
let C = A.then(() => console.log("C"));
B.then(() => console.log("D"));
B.then(() => console.log("E"));
C.then(() => console.log("F"));
C.then(() => console.log("G"));
// A
// B
// C
// D
// E
// F
// G
注意,日志的输出语句是对二叉树的层序遍历。如前所述,期约的处理程序是按照它们添加的顺序 执行的。由于期约的处理程序是先添加到消息队列,然后才逐个执行,因此构成了层序遍历。
树只是期约图的一种形式。考虑到根节点不一定唯一,且多个期约也可以组合成一个期约(通过下 一节介绍的 Promise.all()和 Promise.race()),所以有向非循环图是体现期约连锁可能性的最准确表达
Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个 可迭代对象,返回一个新期约
let p = Promise.all([
Promise.resolve(),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p); // Promise
p.then(() => setTimeout(console.log, 0, 'all() resolved!'));
// all() resolved!(大约 1 秒后)
如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的 期约也会拒绝:
// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise
// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
Promise.resolve(),
Promise.reject(),
Promise.resolve()
]);
setTimeout(console.log, 0, p2); // Promise
// Uncaught (in promise) undefined
// 如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:
let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4)
]);
p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]
Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的 期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约:
let p1 = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
setTimeout(console.log, 0, p1); // Promise : 3
ECMAScript 6 新增了正式的 Promise(期约)引用类型,支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用 async 和 await 关键字定义异步函数的机制
同步操作与异步操作更是代码所要依赖的核心机制。异步行为是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定,那么这样做就是务实的。
异步操作并不一定计算量大或要等很长时间。只要你不想为等待某个异步操作而阻塞线 程执行,那么任何时候都可以使用。
回调函数是一种特殊的函数,它作为参数传递给另一个函数,并在被调用函数执行完毕后被调用。回调函数通常用于事件处理、异步编程和处理各种操作系统和框架的API。
假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?
function double(value, callback) {
setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`I was given: ${x}`));
// ?
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== "number") {
throw "Must provide number as first argument";
}
success(2 * value);
} catch (e) {
failure(e);
}
}, 1000);
}
const successCallback = (x) => {
double(x, (y) => console.log(`Success: ${y}`));
};
const failureCallback = (e) => console.log(`Failure: ${e}`);
double(3, successCallback, failureCallback);
// ?
创建新期约时需要传入 **执行器(executor)**函数作为参数
let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise
待定(pending)是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现 (fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约的状态就不再改变。
而且,也不能保证期约必然会脱离待定状态。
由于期约的状态是私有的,所以只能在内部进行操作。内部操作在期约的执行器函数中完成。执行 器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。其中,控制期约状态的转换是 通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用 resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛 出错误(后面会讨论这个错误)
let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 0, p1); // Promise
let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2); // Promise
// Uncaught error (in promise)
执行器函数是同步执行的
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
setTimeout(console.log, 0, p);
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。
let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
// true
try {
throw new Error('foo');
} catch(e) {
console.log(e); // Error: foo
}
try {
Promise.reject(new Error('bar'));
} catch(e) {
console.log(e);
}
// Uncaught (in promise) Error: bar
第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到。乍一看这可能 有点违反直觉,因为代码中确实是同步创建了一个拒绝的期约实例,而这个实例也抛出了包含拒绝理由 的错误。这里的同步代码之所以没有捕获期约抛出的错误,是因为它没有通过异步模式捕获错误。从这 里就可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。 ??
** 在前面的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队 列来处理的。因此,try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互 的方式就是使用异步结构——更具体地说,就是期约的方法 **
- Promise.prototype.then() Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个 then()方法接收最多 两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话, 则会在期约分别进入“兑现”和“拒绝”状态时执行
function onResolved(id) {
setTimeout(console.log, 0, id, "resolved");
}
function onRejected(id) {
setTimeout(console.log, 0, id, "rejected");
}
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
p1.then(
() => onResolved("p1"),
() => onRejected("p1")
);
p2.then(
() => onResolved("p2"),
() => onRejected("p2")
);
//(3 秒后)
// p1 resolved
// p2 rejected
Promise.prototype.then()方法返回一个新的期约**实例 **
这个新期约实例基于 onResovled 处理程序的返回值构建。换句话说,该处理程序的返回值会通过 Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会 包装上一个期约解决之后的值。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回 值 undefined。** **
let p1 = Promise.resolve("foo");
// 若调用 then()时不传处理程序,则原样向后传
let p2 = p1.then();
setTimeout(console.log, 0, p2); // Promise : foo
// 这些都一样
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise : undefined
setTimeout(console.log, 0, p4); // Promise : undefined
setTimeout(console.log, 0, p5); // Promise : undefined
// 如果有显式的返回值,则 Promise.resolve()会包装这个值
// 这些都一样
let p6 = p1.then(() => "bar");
let p7 = p1.then(() => Promise.resolve("bar"));
setTimeout(console.log, 0, p6); // Promise : bar
setTimeout(console.log, 0, p7); // Promise : bar
// Promise.resolve()保留返回的期约
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p8); // Promise
setTimeout(console.log, 0, p9); // Promise : undefined
// 抛出异常会返回拒绝的期约:
let p10 = p1.then(() => {
throw "baz";
});
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise baz
//QA:注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中: (抛出异常,和返回错误值是不一样的!!!)
let p11 = p1.then(() => Error("qux"));
setTimeout(console.log, 0, p11); // Promise : Error: qux
Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数: onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)
// 下面的代码展示了这两种同样的情况:
let p = Promise.reject();
let onRejected = function(e) {
setTimeout(console.log, 0, 'rejected');
};
// 这两种添加拒绝处理程序的方式是一样的:
p.then(null, onRejected); // rejected
p.catch(onRejected); // rejected
Promise.prototype.finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期 约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出 现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用 于添加清理代码
let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function() {
setTimeout(console.log, 0, 'Finally!')
}
p1.finally(onFinally); // Finally
p2.finally(onFinally); // Finally
// Promise.prototype.finally()方法返回一个新的期约实例:
let p1 = new Promise(() => {});
let p2 = p1.finally();
setTimeout(console.log, 0, p1); // Promise
setTimeout(console.log, 0, p2); // Promise
setTimeout(console.log, 0, p1 === p2); // false
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处 理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联 的状态,执行顺序也是这样的。这个特性由 JavaScript 运行时保证,被称为“非重入”(non-reentrancy) 特性。下面的例子演示了这个特性
// 创建解决的期约
let p = Promise.resolve();
// 添加解决处理程序
// 直觉上,这个处理程序会等期约一解决就执行
p.then(() => console.log('onResolved handler'));
// 同步输出,证明 then()已经返回
console.log('then() returns');
// 实际的输出:
// then() returns
// onResolved handler
在这个例子中,在一个解决期约上调用 then()会把 onResolved 处理程序推进消息队列。但这个 处理程序在当前线程上的同步代码执行完成前不会执行。因此,跟在 then()后面的同步代码一定先于 处理程序执行。
从简单的输入验证脚本到强大的编程语言,JavaScript 的崛起没有任何人预测到。它很简单,学会 用只要几分钟;它又很复杂,掌握它要很多年。要真正学好用好 JavaScript,理解其本质、历史及局限性是非常重要的
它的主要用途是代替 Perl 等服务器端语言处理输入验证
当时,大多数用户使用 28.8kbit/s 的 调制解调器上网
1995 年,网景公司一位名叫 Brendan Eich 的工程师,开始为即将发布的 Netscape Navigator 2 开发一 个叫 Mocha(后来改名为 LiveScript)的脚本语言。当时的计划是在客户端和服务器端都使用它,它在 服务器端叫 LiveWire。 为了赶上发布时间,网景与 Sun 公司结为开发联盟,共同完成 LiveScript 的开发。就在 Netscape Navigator 2 正式发布前,网景把 LiveScript 改名为 JavaScript,以便搭上媒体当时热烈炒作 Java 的顺风车。
由于 JavaScript 1.0 很成功,网景又在 Netscape Navigator 3 中发布了 1.1 版本。尚未成熟的 Web 的受欢迎程度达到了历史新高,而网景则稳居市场领导者的位置。这时候,微软决定向 IE 投入更多资源。 就在 Netscape Navigator 3 发布后不久,微软发布了 IE3,其中包含自己名为 JScript(叫这个名字是为了 避免与网景发生许可纠纷)的 JavaScript 实现。1996 年 8 月,微软重磅进入 Web 浏览器领域,这是网景永远的痛,但它代表 JavaScript 作为一门语言向前迈进了一大步
微软的 JavaScript 实现意味着出现了两个版本的 JavaScript:Netscape Navigator 中的 JavaScript,以 及 IE 中的 JScript。与 C 语言以及很多其他编程语言不同,JavaScript 还没有规范其语法或特性的标准, 两个版本并存让这个问题更加突出了。随着业界担忧日甚,JavaScript 终于踏上了标准化的征程。
1997 年,JavaScript 1.1 作为提案被提交给欧洲计算机制造商协会(Ecma)。第 39 技术委员会(TC39) 承担了“标准化一门通用、跨平台、厂商中立的脚本语言的语法和语义”的任务(参见 TC39-ECMAScript)。 TC39 委员会由来自网景、Sun、微软、Borland、Nombas 和其他对这门脚本语言有兴趣的公司的工程师 组成。他们花了数月时间打造出 ECMA-262,也就是 ECMAScript(发音为“ek-ma-script”)这个新的脚本语言标准。 1998 年,国际标准化组织(ISO)和国际电工委员会(IEC)也将 ECMAScript 采纳为标准(ISO/ IEC-16262)。自此以后,各家浏览器均以 ECMAScript 作为自己 JavaScript 实现的依据,虽然具体实现 各有不同。
ECMA-262 到底定义了什么?在基本的层面,它描述这门语言的如下部分:
ECMAScript 只是对实现这个规范描述的所有方面的一门语言的称呼。JavaScript 实现了 ECMAScript,而 Adobe ActionScript 同样也实现了 ECMAScript。
- ECMAScript 不同的版本以“edition”表示(也就是描述特定实现的 ECMA-262 的版本)。
- ECMA-262 最近的版本是第 14 版,发布于 2023 年 6 月。
ECMA-262 - Ecma International
- ECMA-262 的第 1 版本质上跟网景的 JavaScript 1.1 相同, 只不过删除了所有浏览器特定的代码,外加少量细微的修改。ECMA-262 要求支持 Unicode 标准(以支 持多语言),而且对象要与平台无关(Netscape JavaScript 1.1 的对象不是这样,比如它的 Date 对象就依 赖平台)。这也是 JavaScript 1.1 和 JavaScript 1.2 不符合 ECMA-262 第 1 版要求的原因。
- ECMA-262 第 2 版只是做了一些编校工作,主要是为了更新之后严格符合 ISO/IEC-16262 的要求, 并没有增减或改变任何特性。ECMAScript 实现通常不使用第 2 版来衡量符合性(conformance)。
- ECMA-262 第 3 版第一次真正对这个标准进行更新,更新了字符串处理、错误定义和数值输出。此 外还增加了对正则表达式、新的控制语句、try/catch 异常处理的支持,以及为了更好地让标准国际化 所做的少量修改。对很多人来说,这标志着 ECMAScript 作为一门真正的编程语言的时代终于到来了。
- ECMA-262 第 4 版是对这门语言的一次彻底修订。作为对 JavaScript 在 Web 上日益成功的回应,开 发者开始修订 ECMAScript 以满足全球 Web 开发日益增长的需求。为此,Ecma T39 再次被召集起来, 以决定这门语言的未来。结果,他们制定的规范几乎在第 3 版基础上完全定义了一门新语言。第 4 版包 括强类型变量、新语句和数据结构、真正的类和经典的继承,以及操作数据的新手段。 与此同时,TC39 委员会的一个子委员会也提出了另外一份提案,叫作“ECMAScript 3.1”,只对这 门语言进行了较少的改进。这个子委员会的人认为第 4 版对这门语言来说跳跃太大了。因此,他们提出 了一个改动较小的提案,只要在现有 JavaScript 引擎基础上做一些增改就可以实现。最终,ES3.1 子委员 会赢得了 TC39 委员会的支持,ECMA-262 第 4 版在正式发布之前被放弃。
- ECMAScript 3.1 变成了 ECMA-262 的第 5 版,于 2009 年 12 月 3 日正式发布。第 5 版致力于厘清 第 3 版存在的歧义,也增加了新功能。新功能包括原生的解析和序列化 JSON 数据的 JSON 对象、方便 继承和高级属性定义的方法,以及新的增强 ECMAScript 引擎解释和执行代码能力的严格模式。第 5 版 在 2011 年 6 月发布了一个维护性修订版,这个修订版只更正了规范中的错误,并未增加任何新的语言 或库特性。
- ECMA-262 第 6 版,俗称 ES6、ES2015 或 ES Harmony(和谐版),于 2015 年 6 月发布。这一版包 含了大概这个规范有史以来最重要的一批增强特性。ES6 正式支持了类、模块、迭代器、生成器、箭头 函数、期约、反射、代理和众多新的数据类型。
- ECMA-262 第 7 版,也称为 ES7 或 ES2016,于 2016 年 6 月发布。这次修订只包含少量语法层面的 增强,如 Array.prototype.includes 和指数操作符。
- ECMA-262 第 8 版,也称为 ES8、ES2017,完成于 2017 年 6 月。这一版主要增加了异步函数
(async/ await)
、SharedArrayBuffer
及Atomics API
,以及Object.values()/Object.entries()/Object. getOwnPropertyDescriptors()
和字符串填充方法,另外明确支持对象字面量最后的逗号。- ECMA-262 第 9 版,也称为 ES9、ES2018,发布于 2018 年 6 月。这次修订包括异步迭代、剩余和 扩展属性、一组新的正则表达式特性、
Promise finally()
,以及模板字面量修订。- ECMA-262第 10版,也称为 ES10、ES2019,发布于 2019年 6月。这次修订增加了
Array.prototype. flat()/flatMap()
、String.prototype.trimStart()/trimEnd()
、Object.fromEntries()
方 法,以及Symbol.prototype.description
属性,明确定义了Function.prototype.toString()
的返回值并固定了Array.prototype.sort()
的顺序。另外,这次修订解决了与 JSON 字符串兼容的 问题,并定义了 catch 子句的可选绑定。
应用编程接口
可通过“⌘+K”插入引用链接,或使用“本地文件”引入源文件。
JavaScript高级程序设计(第4版)