一、HTML中的Javascript
1、<script>元素
1.1 <script>的8个属性
**async:**可选。表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其 他脚本加载
charset:可选。使用src属性指定的代码字符集。这个属性很少使用,因为大多浏览器不在乎它的值
**crossorigin:**可选。配置相关请求的CORS(跨源资源共享)设置。默认不使用CORS。crossorigin= "anonymous"配置文件请求不必设置凭据标志。crossorigin="use-credentials"设置凭据 标志,意味着出站请求会包含凭据。
**defer:**可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。 在 IE7 及更早的版本中,对行内脚本也可以指定这个属性。
integrity: 可选。允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI, Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错, 脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提 供恶意内容。
language: 废弃。最初用于表示代码块中的脚本语言 (如"JavaScript"、"JavaScript 1.2" 或"VBScript")。大多数浏览器都会忽略这个属性,不应该再使用它。
**src: **可选。表示包含要执行的代码的外部文件。
**type:**可选。代替 language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。按照惯 例,这个值始终都是"text/javascript",尽管"text/javascript"和"text/ecmascript" 都已经废弃了。
1.2 使用script的两种方式
写在head标签里面
这样写的话会先加载head里面script的js文件,然后再渲染页面,会有白屏出现
写在body标签里面
这样写的话会先渲染页面,再加载js文件,注意要把渲染页面的标签写在script标签前面,否则会先加载js文件,不会出现白屏
1.3 推迟执行脚本
HTML 4.01 为<script>元素定义了一个叫 defer 的属性。这个属性表示脚本在执行的时候不会改变页面的结构。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>元素上设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行。
1.4 XTML中的变化
再xtml中, a<b, '<'会被解释成标签的开始,并且由于作为标签开始的小于号后面不能有空格, 这会导致语法错误。
解决方法:
<script type="text/javascript">
<![CDATA[
if (a < b) {
console.log("A is less than B");
}
]]></script>
在兼容 XHTML 的浏览器中,这样能解决问题。但在不支持 CDATA 块的非 XHTML 兼容浏览器中 则不行。为此,CDATA 标记必须使用 JavaScript 注释来抵消:
<script type="text/javascript">
//<![CDATA[
if (a < b) {
console.log("A is less than B");
}
//]]>
</script>
这种格式适用于所有现代浏览器。虽然有点黑科技的味道,但它可以通过 XHTML 验证,而且对 XHTML 之前的浏览器也能优雅地降级
2、<noscript>元素
对于不支持javascript的浏览器可以用<noscript>,相当于一句警告
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script defer="defer" src="example1.js"></script>
<script defer="defer" src="example2.js"></script>
</head>
<body>
<noscript>
<p>浏览器不支持javascript</p>
</noscript>
</body>
</html>
二、语言基础
1、语法
1.1区分大小写
比如test和Test是两个不同的变量
1.2标识符
**1)**第一个字符必须是一个字母、下划线(_)或美元符号($)
**2)**剩下的其他字符可以是字母、下划线、美元符号或数字。
2、数据类型
ECMAScript 有 6 种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、 String 和 Symbol。Symbol(符号)是 ECMAScript 6 新增的。还有一种复杂数据类型叫 Object(对 象)。
一切复杂数据类型都是由基本数据类型构成的。这也是浅拷贝和深拷贝的原理
1.1 typeof操作符
"undefined"表示值未定义;
"boolean"表示值为布尔值;
"string"表示值为字符串;
"number"表示值为数值;
"object"表示值为对象(而不是函数)或 null;
"function"表示值为函数;
"symbol"表示值为符号。
1.2 Undefined类型
当使用var或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值
对未声明的变量,只能执行一个有用的操作,就是对它调用 typeof。
let a;
console.log(a); //报错
let b;
console.log(typeof b); //正常执行,输出undefined
无论是声明还是未声明,typeof 返回的都是字符串"undefined"。
1.3 null类型
Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因:
let car = null;
console.log(typeof car); // "object"
用等于操作符(==)比较 null 和 undefined 始终返回 true。这是因为这个操作符为了比较而转换了它的操作数
null和undefined的区别:如果变量想要保存对象,而当时又没有那个对象可保存,就要用null来填充该变量。这样就可以保持null时空对象指针的语义,从而进一步将其与undefined区分开来
1.4 Boolean类型
Boolean(布尔值):true和false。这两个布尔值不同于数值,因此true不等于1,false不等于0
| 数据类型 | 转换为true的值 | 转换为false的值 |
|---|---|---|
| Boolean | true | false |
| String | 非空字符串 | ""(空字符串) |
| Number | 非零数值 | 0、NaN |
| Object | 任意对象 | null |
| Undefined | 没有 | undefined |
1.5 Number类型
NaN
有一个特殊的数值叫 NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。0、+0或-0相除会返回NaN:
console.log(0/0); // NaN
console.log(-0/+0); // NaN
如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity 或-Infinity:
console.log(5/0); // Infinity
console.log(5/-0); // -Infinity
NaN的独特属性:
**1)**任何涉及 NaN 的操作始终返回 NaN,哪怕你前面99个运算都是对的,但后面只要有一个操作为NaN,那么输出的值就为NaN
2) NaN 不等于包括 NaN 在内的任何值。
console.log(NaN == NaN); //false
isNaN()函数
该函数接收一个参数,之后会对该参数进行一个数值转化,如果不能转化为数值,那么就返回true
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10 是数值
console.log(isNaN("10")); // false,可以转换为数值 10
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值 1
数值转化
有 3 个函数可以将非数值转换为数值:
Number()
parseInt()
parseFloat()
Number()是 转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。
Number()函数
布尔值:true转化为1,false转化为0
数值:直接返回
null:返回0
undefined:返回NaN
字符串:
- 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。 因此,Number("1")返回 1,Number("123")返回 123,Number("011")返回 11(忽略前面 的零)
- 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
- 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整 数值。
- 如果是空字符串(不包含字符),则返回 0。
- 如果字符串包含除上述情况之外的其他字符,则返回 NaN。
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
parseInt()函数
如果第一个字符不是数值字符、加号或减号,parseInt()立即 返回 NaN。且能识别不同的整数格式(十进制、八 进制、十六进制)
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); //NaN
let num3 = parseInt("x1"); //NaN
let num4 = parseInt("0xA"); //10解释成十进制
parseInt()函数第二个参数
let num1 = parseInt("10", 2); // 2,按二进制解析
let num2 = parseInt("10", 8); // 8,按八进制解析
let num3 = parseInt("10", 10); // 10,按十进制解析
let num4 = parseInt("10", 16); // 16,按十六进制解析
因为不传底数参数相当于让 parseInt()自己决定如何解析,所以为避免解析出错,建议始终传给 它第二个参数。
parseFloat()函数
parseFloat()函数的工作方式跟 parseInt()函数类似,都是从位置 0 开始检测每个字符。
parseFloat()函数只识别十进制格式
如果字符串表示整数(没有小数点或者小 数点后面只有一个零),则 parseFloat()返回整数。
1.6 string类型
String()函数
-
如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
-
如果值是 null,返回"null"。
-
如果值是 undefined,返回"undefined"。
console.log(String(10)); // "10"
console.log(String(true)); // "true"
console.log(String(null)); // "null"
console.log(String(value)); // "undefined"
模板字面量标签函数
模板字面量:``
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar';
}
let untaggedResult = `${a} + ${b} = ${a + b}`;
simpleTag`${untaggedResult}`
console.log('================');
simpleTag`${a} + ${b} = ${a + b}`
输出结果为:
strings:
aValExpression:{a}
bValExpression: {b}
sumExpression: {c}
第一个参数表示所有的{}与``的间隙字符,随后每一个参数都对应一个{}中的值
原始字符串
比如\n表示换行符,const a = \n,把a打印出来是个换行符
,所以此时可以用string.raw标签函数
const a = string.raw\n ,再打印值就是‘\n’了
另外,也可以通过标签函数的第一个参数,即字符串数组的.raw 属性取得每个字符串的原始内容:
function printRaw(strings) {
console.log('Actual characters:');
for (const string of strings) {
console.log(string);
}
console.log('Escaped characters;');
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${ 'and' }\n`;
打印结果为:
3、操作符
1.1 一元运算符
只操作一个值的操作符叫一元运算符。
let s1 = "2";
let s2 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1++; // 值变成数值 3
s2++; // 值变成 NaN
b++; // 值变成数值 1
f--; // 值变成 0.10000000000000009(因为浮点数不精确)
o--; // 值变成-2
1.2 布尔操作符
逻辑与&&
let found = true;
let result = (found && someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行
let found = false;
let result = (found && someUndeclaredVariable); // 不会出错
console.log(result); // 会执行
逻辑或||
- 如果第一个操作数是对象,则返回第一个操作数。
- 如果第一个操作数求值为 false,则返回第二个操作数。
- 如果两个操作数都是对象,则返回第一个操作数。
利用这个行为,可以避免给变量赋值 null 或 undefined。比如:
let myObject = preferredObject || backupObject;
在这个例子中,变量 myObject 会被赋予两个值中的一个。其中,preferredObject 变量包含首 选的值,backupObject 变量包含备用的值。如果 preferredObject 不是 null,则它的值就会赋给 myObject;如果 preferredObject 是 null,则 backupObject 的值就会赋给 myObject。这种模式在 ECMAScript 代码中经常用于变量赋值,
1.3 乘法、除法操作符
如果有不是数值的操作数,则先在后台用 Number()将其转换为数值,然后再应用上述规则。
比如:
const a = '8' * true;
console.log(a); //8
1.4 加法操作符
这里的+成了拼接字符串
let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + num1 + num2;
console.log(message); // "The sum of 5 and 10 is 510"
正确做法:
let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + (num1 + num2);
console.log(message); // "The sum of 5 and 10 is 15"
1.5 关系操作符
如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
let result = "Brick" < "alphabet"; //true
在这里,字符串"Brick"被认为小于字符串"alphabet",因为字母 B 的编码是 66,字母 a 的编码 是 97。要得到确实按字母顺序比较的结果,就必须把两者都转换为相同的大小写形式(全大写或全小写), 然后再比较:
let result = "Brick".toLowerCase() < "alphabet".toLowerCase(); // false
如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
let result = "23" < "3"; // true
let result = "23" < 3; // false
如果有任一操作数是布尔值,则将其转换为数值再执行比较。
let result = 0 < true //true
在比较 NaN 时, 无论是小于还是大于等于,比较的结果都会返回 false。
let result1 = NaN < 3; // false
let result2 = NaN >= 3; // false
1.6 相等操作符
等于(==)和不等于(!=)
这两个操作符都会先进 行类型转换(通常称为强制类型转换)再确定操作数是否相等。
全等(===)和不全等(!==)
它们在比较相等时不转换操作数
let result1 = ("55" == 55); // true,转换后相等
let result2 = ("55" === 55); // false,不相等,因为数据类型不同
1.7 标签语句
let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break outermost
}
num++;
}
}
console.log(num); // 55
不加标签的话
let num = 0;
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break
}
num++;
}
}
console.log(num); // 95
continue是跳出当前循环,break是跳出循环体
三、变量,作用域,内存
1、确定类型
typeof null; // object
typeof [0,1,2]; // object
typeof 虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象, 而是想知道它是什么类型的对象。为了解决这个问题,ECMAScript 提供了 instanceof 操作符,语法如下:
[1,2,3] instanceof Object; //true
[1,2,3] instanceof Array; //true
2、隐藏类和删除类
谷歌浏览器:v8引擎
运行期间,V8 会将创建的对象与隐藏类(Article)关联起来,以跟踪它们的属性特征。能够共享相同隐藏类 的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。比如下面的代码:
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article();
V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原 型。假设之后又添加了下面这行代码:
a2.author = 'Jake';
此时两个 Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有 可能对性能产生明显影响。
当然,解决方案就是避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在 构造函数中一次性声明所有属性,如下所示:
function Article(opt_author) {
this.title = 'Inauguration Ceremony Features Kazoo Band';
this.author = opt_author;
}
let a1 = new Article();
let a2 = new Article('Jake');
使用 delete 关键字会导致生成相同的隐藏类片段。看一下这 个例子:
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
delete a1.author;
在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性 与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为 null。这样可以保持隐藏类不变 和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。比如:
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
a1.author = null;
四、基本引用类型
1、原始值包装类型
每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的 各种方法。来看下面的例子:
const s = 'zyf'
console.log(s.length);
s是一个原始值,原始值不是对象,所以身上不应该含有方法,而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。
具体来说,当 第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串 值的任何时候,后台都会执行以下 3 步:
1)创建一个String类型的实例
let s = new String('zyf')
2)调用实例上的特定方法
console.log(s.length)
3)销毁实例
s = null
这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上 3 步也会在后台发生,只不过 使用的是 Boolean 和 Number 包装类型而已。
引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到 的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期 间。这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:
let s1 = "some text";
s1.color = "red";
console.log(s1.color); // undefined
这里的第二行代码尝试给字符串 s1 添加了一个 color 属性。可是,第三行代码访问 color 属性时, 它却不见了。原因就是第二行代码运行时会临时创建一个 String 对象,而当第三行代码执行时,这个对 象已经被销毁了。实际上,第三行代码在这里创建了自己的 String 对象,但这个对象没有 color 属性。
对比:
let s1 = new String("some text");
s1.color = "red";
console.log(s1.color); // red
1.1 Boolean
所有对象在布尔表达式中都会自动转换为 true
let falseObject = new Boolean(false);
let result = falseObject && true;
console.log(result); // true
1.2 Number
Number 类型提供了几个用于将数值格式化为字符串的方法。
toFixed()
该方法返回包含指定小数点位数的数值字符串,如:
let num = 10;
console.log(num.toFixed(2)); // "10.00"
toPrecision()
此方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法 形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。来看几个例子:
let num = 99;
console.log(num.toPrecision(1)); // "100"
console.log(num.toPrecision(2)); // "99"
console.log(num.toPrecision(3)); // "99.0"
因为 99 不能只用 1 位 数字来精确表示,所以这个方法就将它舍入为 100,这样就可以只用 1 位数字(及其科学记数法形式) 来表示了。用 2 位数字表示 99 得到"99",用 3 位数字则是"99.0"。
在处理原始数值和引用数值时,typeof 和 instacnceof 操作符会返回不同的结果,如下所示:
let numberObject = new Number(10);
let numberValue = 10;
console.log(typeof numberObject); // "object"
console.log(typeof numberValue); // "number"
console.log(numberObject instanceof Number); // true
console.log(numberValue instanceof Number); // false
原始数值在调用 typeof 时始终返回"number",而 Number 对象则返回"object"。类似地,Number 对象是 Number 类型的实例,而原始数值不是
isInteger
此方法用于辨别一个数值是否保存为整数。有时候,小数位的 0 可能会让人误以为数值是一个浮点值:
console.log(Number.isInteger(1)); // true
console.log(Number.isInteger(1.00)); // true
console.log(Number.isInteger(1.01)); // false
1.3 String
charAt
charAt()方法返回给定索引位置的字符
let message = "abcde";
console.log(message.charAt(2)); //"c"
字符串位置方法(indexOf和lastIndexOf)
indexOf()方法从字符串开头开始查找子字符串,而 lastIndexOf()方法从字符串末尾开始查找子字符串。
let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4
console.log(stringValue.lastIndexOf("o")); // 7
这两个方法都可以接收可选的第二个参数,表示开始搜索的位置。
let stringValue = "hello world";
console.log(stringValue.indexOf("o", 6)); // 7
console.log(stringValue.lastIndexOf("o", 6)); // 4
这一次,indexOf()返回 7,因为它从位 置 6(字符"w")开始向后搜索字符串,在位置 7 找到了"o"。而 lastIndexOf()返回 4,因为它从位 置 6 开始反向搜索至字符串开头,因此找到了"hello"中的"o"。
在字符串中找到所有的目标子字符串
const stringvalue = 'woshiozyfo';
let a = []
let i = 0
while (i !== -1) {
i = stringvalue.indexOf("o", i + 1)
if (i > 0) {
a.push(i)
}
}
console.log(a);
字符串包含方法
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.endsWith("baz")); // true
console.log(message.endsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false
startsWith()和 includes()方法接收可选的第二个参数,表示开始搜索的位置。
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("foo", 1)); // false
console.log(message.includes("bar")); // true
console.log(message.includes("bar", 4)); // false
repeat()方法
表示要将字 符串复制多少次,然后返回拼接所有副本后的结果。
const stringValue = "na ";
console.log(stringValue.repeat(5) + "batman"); // na na na na na batman
padStart()和 padEnd()方法
padStart()和 padEnd()方法会复制字符串,如+-果小于指定长度,则在相应一边填充字符,直至 满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格
let stringValue = "foo";
console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"
console.log(stringValue.padEnd(6)); // "foo "
console.log(stringValue.padEnd(9, ".")); // "foo......"
如果长度小于或等于字符串长度,则会返回原始字符串。
console.log(stringValue.padStart(2));
字符串大小写转换
toLowerCase()
toLocaleLowerCase()
toUpperCase()
toLocaleUpperCase()
toLocaleLowerCase()和 toLocaleUpperCase()方法旨在基于 特定地区实现。在很多地区,地区特定的方法与通用的方法是一样的。但在少数语言中(如土耳其语), Unicode 大小写转换需应用特殊规则,要使用地区特定的方法才能实现正确转换。
let stringValue = "hello world";
console.log(stringValue.toLocaleUpperCase()); // "HELLO WORLD"
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLocaleLowerCase()); // "hello world"
console.log(stringValue.toLowerCase()); // "hello world"
通常,如果不知道代码涉及什么语言,则最好使用地区特定的转换方法。
localeCompare()方法
let stringValue = "yellow";
console.log(stringValue.localeCompare("brick")); // 1
console.log(stringValue.localeCompare("yellow")); // 0
console.log(stringValue.localeCompare("zoo")); // -1
字符串"yellow"与 3 个不同的值进行了比较:"brick"、"yellow"和"zoo"。"brick" 按字母表顺序应该排在"yellow"前头,因此 localeCompare()返回 1。"yellow"等于"yellow",因 此"localeCompare()"返回 0。最后,"zoo"在"yellow"后面,因此 localeCompare()返回-1。
eval()
eval()方法可能是整个 ECMAScript 语言中最强大的了。这个方法就是一个完整的 ECMAScript 解释器,它接收一个参数,即一个要执行的 ECMAScript(JavaScript)字符串。
eval("console.log('hi')");
上面这行代码的功能与下一行等价:
console.log("hi");
这里,变量 msg 是在 eval()调用的外部上下文中定义的,而 console.log()显示了文本"hello world"。这是因为第二行代码会被替换成一行真正的函数调用代码。类似地,可以在 eval()内部定义 一个函数或变量,然后在外部代码中引用,如下所示:
eval("function sayHi() { console.log('hi'); }");
sayHi();
通过 eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在 一个字符串中的。它们只是在 eval()执行的时候才会被创建。 在严格模式下,在 eval()内部创建的变量和函数无法被外部访问。换句话说,最后两个例子会报 错。同样,在严格模式下,赋值给 eval 也会导致错误:
"use strict";
eval = "hi"; // 导致错误
2、Global对象属性
Global 对象有很多属性,其中一些前面已经提到过了。像 undefined、NaN 和 Infinity 等特殊 值都是 Global 对象的属性。此外,所有原生引用类型构造函数,比如 Object 和 Function,也都是 Global 对象的属性。
3、window对象
浏览器将 window 对象实现为 Global 对象的代理。因此,所有全局作用域中声明的变量和函数都变成了 window 的属性。
var color = "red";
function sayColor() {
console.log(window.color);
}
window.sayColor(); // "red"
3、Math
1.1 Math 对象属性
Math 对象有一些属性,主要用于保存数学中的一些特殊值。下表列出了这些属性。
| 属 性 | 说 明 |
|---|---|
| Math.E | 自然对数的基数 e 的值 |
| Math.LN10 | 10 为底的自然对数 |
| Math.LN2 | 2 为底的自然对数 |
| Math.LOG2E | 以 2 为底 e 的对 |
| Math.LOG10E | 以 10 为底 e 的对数 |
| Math.PI | π 的值 |
| Math.SQRT1_2 | 1/2 的平方根 |
| Math.SQRT2 | 2 的平方根 |
1.2 min()和 max()方法
let max = Math.max(3, 54, 32, 16);
console.log(max); // 54
let min = Math.min(3, 54, 32, 16);
console.log(min); // 3
1.3舍入方法
Math.ceil()方法始终向上舍入为最接近的整数。
Math.floor()方法始终向下舍入为最接近的整数。
Math.round()方法执行四舍五入。
Math.fround()方法返回数值最接近的单精度(32 位)浮点值表示。
console.log(Math.ceil(25.9)); // 26
console.log(Math.ceil(25.5)); // 26
console.log(Math.ceil(25.1)); // 26
console.log(Math.round(25.9)); // 26
console.log(Math.round(25.5)); // 26
console.log(Math.round(25.1)); // 25
console.log(Math.fround(0.4)); // 0.4000000059604645
console.log(Math.fround(0.5)); // 0.5
console.log(Math.fround(25.9)); // 25.899999618530273
console.log(Math.floor(25.9)); // 25
console.log(Math.floor(25.5)); // 25
console.log(Math.floor(25.1)); // 25
五、集合引用类型
1、Array
1.1 Array.from()
可以将类数组结构转换为数组实例,
Array.from()的第一个参数是一个类数组对象,即任何可迭代的结构,或者有一个 length 属性 和可索引元素的结构。这种方式可用于很多场合:
// 字符串会被拆分为单字符数组
console.log(Array.from("Matt")); // ["M", "a", "t", "t"]
// 可以使用 from()将集合和映射转换为一个新数组
const m = new Map().set(1, 2)
.set(3, 4);
const s = new Set().add(1)
.add(2)
.add(3)
.add(4);
const m = new Map().set(1, 2)
.set(3, 4);
const s = new Set().add(1)
.add(2)
.add(3)
.add(4);
// arguments 对象可以被轻松地转换为数组
function getArgsArray() {
return Array.from(arguments);
}
console.log(getArgsArray(1, 2, 3, 4)); // [1, 2, 3, 4]
// from()也能转换带有必要属性的自定义对象
const arrayLikeObject = {
0: 1,
1: 2,
2: 3,
3: 4,
length: 4
};
console.log(Array.from(arrayLikeObject)); // [1, 2, 3, 4]
Array.from()还接收第二个可选的映射函数参数。这个函数可以直接增强新数组的值,而无须像 调用 Array.from().map()那样先创建一个中间数组。还可以接收第三个可选参数,用于指定映射函 数中 this 的值。但这个重写的 this 值在箭头函数中不适用。
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, x => x**2);
const a3 = Array.from(a1, function(x) {return x**this.exponent}, {exponent: 2});
console.log(a2); // [1, 4, 9, 16]
console.log(a3); // [1, 4, 9, 16]
1.2 Array.of()
Array.of()可以把一组参数转换为数组。这个方法用于替代在 ES6之前常用的 Array.prototype. slice.call(arguments),一种异常笨拙的将 arguments 对象转换为数组的写法:
console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
console.log(Array.of(undefined)); // [undefined]
1.3 数组索引
可以通过修改 length 属性,使得数组末尾删除或添加元素:
let colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors.length = 2;
alert(colors[2]); // undefined
使用 length 属性可以方便地向数组末尾添加元素,如下例所示:
let colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors[colors.length] = "black"; // 添加一种颜色(位置 3)
colors[colors.length] = "brown"; // 再添加一种颜色(位置 4)
let colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors[99] = "black"; // 添加一种颜色(位置 99)
alert(colors.length); // 100
这里,colors 数组有一个值被插入到位置 99,结果新 length 就变成了 100(99 + 1)。这中间的 所有元素,即位置 3~98,实际上并不存在,因此在访问时会返回 undefined。
确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。可以用以下例子
if (Array.isArray(value)){
// 操作数组
}
1.4 迭代器
const a = ["foo", "bar", "baz", "qux"];
const aEntries = Array.from(a.entries());
// 打印的值就是迭代器
console.log(aEntries); //[[0, "foo"], [1, "bar"], [2, "baz"], [3, "qux"]]
1.5 复制和填充方法
fill()
使用 fill()方法可以向一个已有的数组中插入全部或部分相同的值。开始索引用于指定开始填充 的位置,它是可选的。如果不提供结束索引,则一直填充到数组末尾。负值索引从数组末尾开始计算。 也可以将负索引想象成数组长度加上它得到的一个正索引:
const zeroes = [0, 0, 0, 0, 0];
// 用 5 填充整个数组
zeroes.fill(5);
console.log(zeroes); // [5, 5, 5, 5, 5]
zeroes.fill(0); // 重置
// 用 6 填充索引大于等于 3 的元素
zeroes.fill(6, 3);
console.log(zeroes); // [0, 0, 0, 6, 6]
zeroes.fill(0); // 重置
// 用 7 填充索引大于等于 1 且小于 3 的元素
zeroes.fill(7, 1, 3);
console.log(zeroes); // [0, 7, 7, 0, 0];
zeroes.fill(0); // 重置
// 用 8 填充索引大于等于 1 且小于 4 的元素
// (-4 + zeroes.length = 1)
// (-1 + zeroes.length = 4)
zeroes.fill(8, -4, -1);
console.log(zeroes); // [0, 8, 8, 8, 0];
fill()静默忽略超出数组边界、零长度及方向相反的索引范围:
const zeroes = [0, 0, 0, 0, 0];
// 索引过低,忽略
zeroes.fill(1, -10, -6);
console.log(zeroes); // [0, 0, 0, 0, 0]
// 索引部分可用,填充可用部分
zeroes.fill(4, 3, 10)
console.log(zeroes); // [0, 0, 0, 4, 4]
copyWithin()
copyWithin()会按照指定范围浅复制数组中的部分内容,然后将它们插入到指 定索引开始的位置。开始索引和结束索引则与 fill()使用同样的计算方法
let ints,
reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();
// 从 ints 中复制索引 0 开始的内容,插入到索引 5 开始的位置
// 在源索引或目标索引到达数组边界时停止
ints.copyWithin(5);
console.log(ints); // [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
reset();
1.6 转换方法
如果想使 用不同的分隔符,则可以使用 join()方法。join()方法接收一个参数,即字符串分隔符,返回包含所 有项的字符串。来看下面的例子:
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
如果数组中某一项是 null 或 undefined,则在 join()、toLocaleString()、 toString()和 valueOf()返回的结果中会以空字符串表示。
1.7 栈方法
栈是一种后进先出(LIFO,Last-In-First-Out)的结构,也就 是最近添加的项先被删除。
push()和pop()
push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。pop()方法则 用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项。
let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2
count = colors.push("black"); // 再推入一项
alert(count); // 3
let item = colors.pop(); // 取得最后一项
alert(item); // black
alert(colors.length); // 2
1.8 队列方法
队列以先进先出(FIFO,First-In-First-Out)形式 限制访问。
shift()
它会删除数组的第一项并返回它,然后数组长度减 1。
let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green");
alert(count); // 2
let item = colors.shift(); // 取得第一项
alert(item); // red
alert(colors.length); // 1
unshift()
执行跟 shift()相反的 操作:在数组开头添加任意多个值,然后返回新的数组长度。
1.9 搜索和位置方法
严格相等
ECMAScript 提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索。
indexOf()、lastIndexOf()和 includes()。其 中,前两个方法在所有版本中都可用,而第三个方法是 ECMAScript 7 新增的。这些方法都接收两个参 数:要查找的元素和一个可选的起始搜索位置。indexOf()和 includes()方法从数组前头(第一项) 开始向后搜索,而 lastIndexOf()从数组末尾(最后一项)开始向前搜索。
indexOf()和 lastIndexOf()都返回要查找的元素在数组中的位置,如果没找到则返回-1。 includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
alert(numbers.indexOf(4)); // 3
alert(numbers.lastIndexOf(4)); // 5
alert(numbers.includes(4)); // true
alert(numbers.indexOf(4, 4)); // 5
alert(numbers.lastIndexOf(4, 4)); // 3
alert(numbers.includes(4, 7)); // false
let person = { name: "Nicholas" };
let people = [{ name: "Nicholas" }];
let morePeople = [person];
alert(people.indexOf(person)); // -1
alert(morePeople.indexOf(person)); // 0
alert(people.includes(person)); // false
alert(morePeople.includes(person)); // true
断言函数
断言函数接收 3 个参数:元素、索引和数组本身。其中元素是数组中当前搜索的元素,索引是当前 元素的索引,而数组就是正在搜索的数组。断言函数返回真值,表示是否匹配。
find()和 findIndex()方法使用了断言函数。这两个方法都从数组的最小索引开始。find()返回 第一个匹配的元素,findIndex()返回第一个匹配元素的索引。这两个方法也都接收第二个可选的参数, 用于指定断言函数内部 this 的值。
const people = [
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
alert(people.find((element, index, array) => element.age < 28));
// {name: "Matt", age: 27}
alert(people.findIndex((element, index, array) => element.age < 28));
// 0 `
找到匹配项后,这两个方法都不再继续搜索。
1.10 迭代方法
-
every():对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true。
-
filter():对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。
-
forEach():对数组每一项都运行传入的函数,没有返回值。
-
map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
-
some():对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true。 这些方法都不改变调用它们的数组。
some():对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true。 这些方法都不改变调用它们的数组。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
alert(everyResult); // false
let someResult = numbers.some((item, index, array) => item > 2);
alert(someResult); // true
2、定型数组
定型数组(typed array)是 ECMAScript新增的结构,目的是提升向原生库传输数据的效率。实际上, JavaScript 并没有“TypedArray”类型,它所指的其实是一种特殊的包含数值类型的数组。
3、Map
Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机 制。Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。
1.1 基本API
使用 new 关键字和 Map 构造函数可以创建一个空映射:
const m = new Map();
如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含键/值对数 组。
const m1 = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
alert(m1.size); // 3
初始化之后,可以使用 set()方法再添加键/值对。另外,可以使用 get()和 has()进行查询,可 以通过 size 属性获取映射中的键/值对的数量,还可以使用 delete()和 clear()删除值。
const m = new Map();
alert(m.has("firstName")); // false
alert(m.get("firstName")); // undefined
alert(m.size); // 0
m.set("firstName", "Matt")
.set("lastName", "Frisbie");
alert(m.has("firstName")); // true
alert(m.get("firstName")); // Matt
alert(m.size); // 2
m.delete("firstName"); // 只删除这一个键/值对
alert(m.has("firstName")); // false
alert(m.has("lastName")); // true
alert(m.size); // 1
m.clear(); // 清除这个映射实例中的所有键/值对
alert(m.has("firstName")); // false
alert(m.has("lastName")); // false
alert(m.size); // 0
set()方法返回映射实例,因此可以把多个操作连缀起来,包括初始化声明:
const m = new Map().set("key1", "val1");
m.set("key2", "val2")
.set("key3", "val3");
alert(m.size); // 3
与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为 键。Map 内部使用 SameValueZero 比较操作(ECMAScript 规范内部定义,语言中不能使用),基本上相 当于使用严格对象相等的标准来检查键的匹配性。与 Object 类似,映射的值是没有限制的。
const m = new Map();
const functionKey = function() {};
const symbolKey = Symbol();
const objectKey = new Object();
m.set(functionKey, "functionValue");
m.set(symbolKey, "symbolValue");
m.set(objectKey, "objectValue");
alert(m.get(functionKey)); // functionValue
alert(m.get(symbolKey)); // symbolValue
alert(m.get(objectKey)); // objectValue
// 地址不同
alert(m.get(function() {})); // undefined
与严格相等一样,在映射中用作键和值的对象及其他“集合”类型,在自己的内容或属性被修改时仍然保持不变(地址没变):
const m = new Map();
const objKey = {},
objVal = {},
arrKey = [],
arrVal = [];
m.set(objKey, objVal); // m = ({},{})
m.set(arrKey, arrVal); // m = ([],[])
objKey.foo = "foo"; // m = ({foo:'foo'},{})
objVal.bar = "bar"; // m = ({foo:'foo'},{bar:'bar'})
arrKey.push("foo"); // m = ([foo],[])
arrVal.push("bar"); // m = ([foo],[bar])
console.log(m.get(objKey)); // {bar: "bar"}
console.log(m.get(arrKey)); // ["bar"]
const m = new Map();
const a = 0/""; // NaN
m.set(a, "foo");
console.log(m.get(NaN)); //foo
1.2 顺序和迭代
映射实例可以提供一个迭代器(Iterator),能以插入顺序生成[key, value]形式的数组。可以 通过 entries()方法(或者 Symbol.iterator 属性,它引用 entries())取得这个迭代器:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
alert(m.entries === m[Symbol.iterator]); // true
for (let pair of m.entries()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
for (let pair of m[Symbol.iterator]()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
因为 entries()是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
console.log([...m]); // [[key1,val1],[key2,val2],[key3,val3]]
如果不使用迭代器,而是使用回调方式,则可以调用映射的 forEach(callback, opt_thisArg) 方法并传入回调,依次迭代每个键/值对。传入的回调接收可选的第二个参数,这个参数用于重写回调 内部 this 的值:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
m.forEach((val, key) => alert(`${key} -> ${val}`));
// key1 -> val1
// key2 -> val2
// key3 -> val3
keys()和 values()分别返回以插入顺序生成键和值的迭代器:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
for (let key of m.keys()) {
alert(key);
}
// key1
// key2
// key3
for (let key of m.values()) {
alert(key);
}
// value1
// value2
// value3
键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。当然,这并不妨碍修改作为 键或值的对象内部的属性,因为这样并不影响它们在映射实例中的身份:
const m1 = new Map([
["key1", "val1"]
]);
// 作为键的字符串原始值是不能修改的
for (let key of m1.keys()) {
key = "newKey";
console.log(key) // newKey
console.log(m1.get("key1")); // val1
console.log(m1.get("newKey")) //undefined
}
// 修改了作为键的对象的属性,但对象在映射内部仍然引用相同的值
for (let key of m.keys()) {
key.id = "newKey";
alert(key); // {id: "newKey"}
alert(m.get(keyObj)); // val1
}
alert(keyObj); // {id: "newKey"}
1.3 选择Object还是Map
-
内存占用(Map完胜)
批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。 不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对。
-
插入性能(Map小胜)
向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快 一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操 作,那么显然 Map 的性能更佳。
-
查找速度(各有优点)
如果只包含少量键/值对, 则 Object 有时候速度更快。
如果代码涉及大量查找操作,那么某些情况下可能选 择 Object 更好一些。
-
删除性能(Map完胜)
使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此, 出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快。 如果代码涉及大量删除操作,那么毫无疑问应该选择 Map。
4、WeakMap
新增的“弱映射”(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/ 值对存储机制。WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集。WeakMap 中的“weak”(弱), 描述的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式。
1.1 基本API
可以使用 new 关键字实例化一个空的 WeakMap:
const wm = new WeakMap();
弱映射中的键只能是 Object 或者继承自 Object 的类型(对象的引用值)
使用非对象设置键会抛出 TypeError。值的类型没有限制。
如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键/值对数组。 可迭代对象中的每个键/值都会按照迭代顺序插入新实例中:
const key1 = {id: 1},
key2 = {id: 2},
key3 = {id: 3};
// 使用嵌套数组初始化弱映射
const wm1 = new WeakMap([
[key1, "val1"],
[key2, "val2"],
[key3, "val3"]
]);
alert(wm1.get(key1)); // val1
alert(wm1.get(key2)); // val2
alert(wm1.get(key3)); // val3
初始化是全有或全无的操作,只要有一个键无效就会抛出错误,导致整个初始化失败
const key1 = { id: 1 },
const wm2 = new WeakMap([
[key1, "val1"],
["BADKEY", "val2"],
]);
// TypeError: Invalid value used as WeakMap key
原始值可以先包装成对象再用作键
const stringKey = new String("key1");
const wm3 = new WeakMap([
stringKey, "val1"
]);
alert(wm3.get(stringKey)); // "val1"
初始化之后可以使用 set()再添加键/值对,可以使用 get()和 has()查询,还可以使用 delete() 删除,这点和Map的使用类似
1.2 弱键
这些键不属于正式的引用, 不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
const wm = new WeakMap();
wm.set({}, "val");
set()方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用, 所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失 了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身 也会成为垃圾回收的目标。
const wm = new WeakMap();
const container = {
key: {}
};
wm.set(container.key, "val");
function removeReference() {
container.key = null;
}
这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目 标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以 把这个键/值对清理掉。
1.3 不可迭代键
WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果 允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
1.4 使用弱映射
私有变量
……
DOM 节点元数据
因为 WeakMap 实例不会妨碍垃圾回收,所以非常适合保存关联元数据。来看下面这个例子,其中 使用了常规的 Map:
const m = new Map();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据
m.set(loginButton, {disabled: true});
假设在上面的代码执行后,页面被 JavaScript 改变了,原来的登录按钮从 DOM 树中被删掉了。但 由于映射中还保存着按钮的引用,所以对应的 DOM 节点仍然会逗留在内存中,除非明确将其从映射中 删除或者等到映射本身被销毁。
如果这里使用的是弱映射,如以下代码所示,那么当节点从 DOM 树中被删除后,垃圾回收程序就 可以立即释放其内存(假设没有其他地方引用这个对象):
const wm = new WeakMap();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据
wm.set(loginButton, {disabled: true});
5、Set
ECMAScript 6 新增的 Set 是一种新集合类型,为这门语言带来集合数据结构。Set 在很多方面都 像是加强的 Map,这是因为它们的大多数 API 和行为都是共有的。
-
Map是键值对的集合
-
Set是数组的集合
1、基本API
除了添加元素和Map不同,其它API都类似都类似
Map添加元素
const m = new Map();
m.set(['id',1])
Set添加元素
const m = new Set();
m.add([1,2,3])
delete()返回一个布尔值,表示集合中是否存在要删除的值:
const s = new Set();
s.add('foo');
alert(s.delete('foo')); // true
alert(s.delete('foo')); // false
6、WeakSet
WeakSet 是 Set 的“兄弟”类型,其API 也是Set的子集。WeakSet 中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱集合”中值的方式。
用法与WeakMap类似,值只能是Object类型或者Object的引用
六 、对象、类与面向对象编程
1、理解对象
1.1 属性的类型
数据属性
- Value
包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。
- writable
表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的 这个特性都是 true,如前面的例子所示。
- Configurable
表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特 性都是 true,如前面的例子所示。
- Enumerable
表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对 象上的属性的这个特性都是 true,如前面的例子所示。
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
这个例子创建了一个名为 name 的属性并给它赋予了一个只读的值"Nicholas"。这个属性的值就 不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性的值会抛出错误。
let person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
delete person.name;
console.log(person.name); // "Nicholas"
与上同理
一个属性被定义为不可配置之后,就不能再变回可配置的了。再次调用Object.defineProperty()并修改任何非 writable 属性会导致错误:
let person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Nicholas"
});
// 抛出错误
Object.defineProperty(person, "name", {
configurable: true,
value: "Nicholas"
});
访问器属性
访问器属性是不能直接定义的,必须使用 Object.defineProperty()。下面是一个例子:
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition = newValue - 2015;
}
}
});
book.year = 2018;
console.log(book.edition); // 3
book.year = 2010;
console.log(book.year) //2018
不能直接修改属性的值
1.2 定义多个属性
在一个对象上同时定义多个属性的可能性是非常大的。为此,ECMAScript 提供了 Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。比如:
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
这段代码在 book 对象上定义了两个数据属性 year_和 edition,还有一个访问器属性 year。 最终的对象跟上一节示例中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的 configurable、enumerable 和 writable 特性值都是 false。
1.3 读取属性的特性
使用 Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接 收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含 configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、 writable 和 value 属性。比如:
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get: function () {
return this.year_;
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()静态方法。这个方法实际上 会在每个自有属性上调用 Object.getOwnPropertyDescriptor()并在一个新对象中返回它们。对于 前面的例子,使用这个静态方法会返回如下对象:
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get: function () {
return this.year_;
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
console.log(Object.getOwnPropertyDescriptors(book));
// {
// edition: {
// configurable: false,
// enumerable: false,
// value: 1,
// writable: false
// },
// year: {
// configurable: false,
// enumerable: false,
// get: f(),
// set: f(newValue),
// },
// year_: {
// configurable: false,
// enumerable: false,
// value: 2017,
// writable: false
// }
// }
1.4 合并对象
Object.assign()
这个方法接收一个目标对象和一个 或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true) 和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象。
const dest = {};
const src = { id: 'src' };
const result = Object.assign(dest, src);
console.log(result); // { id: src }
console.log(dest); // { id: src }
console.log(dest === result); // true
console.log(dest !== src); // true
通过上述案例可以看出:
将对象src的键和值复制到了dest,然后再将dest的地址给了result
获取函数与设置函数
const dest = {
set a(val) {
console.log(`Invoked dest setter with param ${val}`); //Invoked dest setter with param foo
}
};
const src = {
get a() {
console.log('Invoked src getter'); //Invoked src getter
return 'foo';
}
};
Object.assign(dest, src);
console.log(dest);
这里合并之后先调用get()方法,取得值('foo')之后,调用set()方法,并将get取得的值当参数传递给set;这里其实也是这样一个过程:
const a = {
}
a.name = 'zyf'
只是把get方法和set方法屏蔽掉了,但原理和上面例子类似
Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使 用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目 标对象。换句话说,不能在两个对象间转移获取函数和设置函数。
const dest = { id: 'dest' };
const result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' });
// Object.assign 会覆盖重复的属性
console.log(result); // { id: src2, a: foo, b: bar }
对象引用
const dest = {};
const src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); //true
1.5 对象标识及相等判定
这些情况在不同 JavaScript 引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
要确定 NaN 的相等性,必须使用极为讨厌的 isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
为改善这类情况,ECMAScript 6 规范新增了 Object.is(),这个方法与===很像,但同时也考虑 到了上述边界情形。这个方法必须接收两个参数:
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
正确的 0、-0、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
正确的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true
要检查超过两个值,递归地利用相等性传递即可:
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
1.6 对象解构
const obj1 = {name:'zyf'}
const {name} = obj1
可以在解构赋值的同时定义默认值
let person = {
name: 'Matt',
age: 27
};
let { name, job='Software engineer' } = person;
console.log(name); // Matt
console.log(job); // Software engineer
解构并不要求变量必须在解构表达式中声明。不过,如果是给事先声明的变量赋值,则赋值表达式 必须包含在一对括号中:
let personName, personAge;
let person = {
name: 'Matt',
age: 27
};
({ name: personName, age: personAge } = person);
console.log(personName, personAge); //Matt 27
console.log(person); //{name: 'Matt', age: 27}
1.7 嵌套解构
解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let personCopy = {};
// 因为一个对象的引用被赋值给 personCopy,所以修改person.job 对象的属性也会影响 personCopy
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
person.job.title = 'Hacker'
console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
解构赋值可以使用嵌套结构,以匹配嵌套的属性:
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
// 声明 title 变量并将 person.job.title 的值赋给它
let { job: { title } } = person;
console.log(title); // Software engineer
2、创建对象
工厂模式
就是函数传参模式
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer")
构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
person1 和 person2 分别保存着 Person 的不同实例。这两个对象都有一个 constructor 属性指向 Person,
创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
constructor就是构造函数本身
function Person(name, age, job) {
console.log(this);
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
};
}
console.log(person1.constructor);
console.log(Person);
console.log(person1.constructor===Person); //true
任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到 window 对象
window.sayName(); // "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
七、代理与反射
1、代理基础
1.1 创建空代理
最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。
代理是使用Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。缺少其中任何一个参数都会抛出 TypeError。
如下面的代码所示,在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象。
const target = {
id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
给目标属性赋值会反映在两个对象上,因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id); // foo
给代理属性赋值会反映在两个对象, 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id); // bar
hasOwnProperty()方法在两个地方都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty('id')); // true
Proxy.prototype 是 undefined,因此不能使用 instanceof 操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
严格相等可以用来区分代理和目标
console.log(target === proxy); // false
1.2 定义捕获器
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的 拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接 或间接在代理对象上调用。
const target = {
foo: 'bar',
name: 'zyf'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
get() {
return 'handler override';
}
};
const proxy = new Proxy(target, handler);
console.log(target.foo); //bar
console.log(proxy.foo); //handler override
console.log(target.name); //zyf
console.log(proxy.name); //handler override
get()不是 ECMAScript 对象可以调用的方法。。proxy[property]、proxy.property 或 Object.create(proxy)[property]等操作都 会触发基本的 get()操作以获取属性。因此所有这些操作只要发生在代理对象上,就会触发 get()捕获 器。注意,只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作值不会变化
1.3 捕获器参数和反射API
get() 捕获器会接收到目标对象、要查询的属性和代理对象三个参数
const target = {
foo: 'bar',
sex: 'male'
};
const handler = {
get(trapTarget, property, receiver) {
console.log(trapTarget === target); //true
console.log(property); //male
console.log(receiver === proxy); //true
}
};
const proxy = new Proxy(target, handler);
proxy.male
复制目标对象(捕获所有方法):
须知:
const handler = {
get(trapTarget, property, receiver) {
return trapTarget[property];
}
};
handler和Reflect是相等的
版本一:
const target = {
foo: 'bar'
};
const handler = {
get() {
return Reflect.get(...arguments);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
版本二:
const target = {
foo: 'bar'
};
const handler = {
get: Reflect.get
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
版本三:
const target = {
foo: 'bar'
};
const proxy = new Proxy(target, Reflect);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
用法:
反射 API 为开发者准备好了样板代码,在此基础上开发者可以用最少的代码修改捕获的方法。比如, 下面的代码在某个属性被访问时,会对返回的值进行一番修饰:
const target = {
foo: 'bar',
baz: 'qux'
};
const handler = {
get(trapTarget, property, receiver) {
let decoration = '';
if (property === 'foo') {
decoration = '!!!';
}
return Reflect.get(...arguments) + decoration;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar!!!
console.log(target.foo); // bar
console.log(proxy.baz); // qux
console.log(target.baz); // qux
1.4 捕获器不变式
每个 捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式” (trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。
如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError:
const target = {};
Object.defineProperty(target, 'foo', {
configurable: false,
writable: false,
value: 'bar'
});
const handler = {
get() {
return 'qux';
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // TypeError
1.5 可撤销代理
revocable()
Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的 操作是不可逆的。而且,撤销函数(revoke())是幂等的,调用多少次的结果都一样。撤销代理之后 再调用代理会抛出 TypeError。
撤销函数和代理对象是在实例化时同时生成的:
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
1.6 实用反射API
Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。
状态标记(反射的一个小作用)
很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。有时候,状态标记 比那些返回修改后的对象或者抛出错误(取决于方法)的反射 API 方法更有用。例如,可以使用反射 API 对下面的代码进行重构:
const o = {};
try {
Object.defineProperty(o, 'foo', 'bar');
console.log('success');
} catch(e) {
console.log('failure');
}
在定义新属性时如果发生问题,Reflect.defineProperty()会返回 false,而不是抛出错误。 因此使用这个反射方法可以这样重构上面的代码:
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 操作符。
在通过 apply 方法调用函数时,被调用的函数可能也定义了自己的 apply 属性(虽然可能性极小)。 为绕过这个问题,可以使用定义在 Function 原型上的 apply 方法,比如:
Function.prototype.apply.call(myFunc, thisVal, argumentList);
这种可怕的代码完全可以使用 Reflect.apply 来避免:
Reflect.apply(myFunc, thisVal, argumentsList);
1.7 代理另一个代理(代理套娃)
代理可以拦截反射 API 的操作,而这意味着完全可以创建一个代理,通过它去代理另一个代理。这 样就可以在一个目标对象之上构建多层拦截网:
const target = {
foo: 'bar'
};
const firstProxy = new Proxy(target, {
get() {
console.log('first proxy');
return Reflect.get(...arguments);
}
});
const secondProxy = new Proxy(firstProxy, {
get() {
console.log('second proxy');
return Reflect.get(...arguments);
}
});
console.log(secondProxy.foo);
// second proxy
// first proxy
// bar
2、代理捕获器与反射方法
1.1 get()
get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()
const myTarget = {foo:'zyf'};
const proxy = new Proxy(myTarget, {
get(target, property, receiver) {
console.log('get()');
return Reflect.get(...arguments)
}
});
console.log(proxy.foo);
//get()
//zyf
1.2 set()
set()捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
console.log('set()');
return Reflect.set(...arguments)
}
});
console.log(proxy.foo = 'zyf');
1.3 has()
has()捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
has(target, property) {
console.log('has()');
return target[property]
}
});
console.log('foo' in proxy);
has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。
1.4 defineProperty()
defineProperty()捕获器会在 Object.defineProperty()中被调用。对应的反射 API 方法为 Reflect.defineProperty()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
defineProperty(target, property, descriptor) {
console.log('defineProperty()');
return Reflect.defineProperty(...arguments)
}
});
Object.defineProperty(proxy, 'foo', { value: 'bar' });
……
3、代理模式
1.1 跟踪属性访问
通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获 器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:
const user = {
name: 'Jake'
};
const proxy = new Proxy(user, {
get(target, property, receiver) {
console.log(`Getting ${property}`);
return Reflect.get(...arguments);
},
set(target, property, value, receiver) {
console.log(`Setting ${property}=${value}`);
return Reflect.set(...arguments);
}
});
proxy.name; // Getting name
proxy.age = 27; // Setting age=27
1.2 隐藏属性
代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。比如:
const hiddenProperties = ['foo', 'bar'];
const targetObject = {
foo: 1,
bar: 2,
baz: 3
};
const proxy = new Proxy(targetObject, {
get(target, property) {
if (hiddenProperties.includes(property)) {
return undefined;
} else {
return Reflect.get(...arguments);
}
},
has(target, property) {
if (hiddenProperties.includes(property)) {
return false;
} else {
return Reflect.has(...arguments);
}
}
});
// get()
console.log(proxy.foo); // undefined
console.log(proxy.bar); // undefined
console.log(proxy.baz); // 3
// has()
console.log('foo' in proxy); // false
console.log('bar' in proxy); // false
console.log('baz' in proxy); // true
1.3 属性验证
因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值:
const target = {
onlyNumbersGoHere: 0
};
const proxy = new Proxy(target, {
set(target, property, value) {
if (typeof value !== 'number') {
return false;
} else {
return Reflect.set(...arguments);
}
}
});
proxy.onlyNumbersGoHere = '';
console.log(proxy.onlyNumbersGoHere); // 0
proxy.onlyNumbersGoHere = 1;
console.log(proxy.onlyNumbersGoHere); // 1
1.4 函数与构造函数参数验证
跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种 类型的值:
function median(...nums) {
return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy = new Proxy(median, {
apply(target, thisArg, argumentsList) {
for (const arg of argumentsList) {
if (typeof arg !== 'number') {
throw 'Non-number argument provided';
}
}
return Reflect.apply(...arguments);
}
});
console.log(proxy(4, 7, 1)); // 4
console.log(proxy(4, '7', 1));
// Error: Non-number argument provided
类似地,可以要求实例化时必须给构造函数传参
class User {
constructor(id) {
this.id_ = id;
}
}
const proxy = new Proxy(User, {
construct(target, argumentsList, newTarget) {
if (argumentsList[0] === undefined) {
throw 'User cannot be instantiated without id';
} else {
return Reflect.construct(...arguments);
}
}
});
new proxy(1);
new proxy();
// Error: User cannot be instantiated without id
1.5 数据绑定与可观察对象
通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的 代码互操作。
比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:
const userList = [];
class User {
constructor(name) {
this.name_ = name;
}
}
const proxy = new Proxy(User, {
construct() {
const newUser = Reflect.construct(...arguments);
userList.push(newUser);
return newUser;
}
});
new proxy('John');
new proxy('Jacob');
new proxy('Jingleheimerschmidt');
console.log(userList); // [User {name_: 'John'}, User {name_: 'Jacob'}, User{name_: 'Jingleheimerschmidt'}]
另外,还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:
const userList = [];
function emit(newValue) {
console.log(newValue);
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver) {
const result = Reflect.set(...arguments);
if (result) {
emit(Reflect.get(target, property, receiver));
}
return result;
}
});
proxy.push('John');
// John
proxy.push('Jacob');
// Jacob
4、总结
开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟 踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。
八、函数
1、箭头函数
不多赘述
2、函数名
function foo() { }
console.log(foo.bind(null).name); // bound foo
let dog = {
years: 1,
get age() {
return this.years;
},
set age(newAge) {
this.years = newAge;
}
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name); // get age
console.log(propertyDescriptor.set.name); // set age
ECMAScript 6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。多数情 况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称, 也会如实显示成空字符串。如果它是使用 Function 构造函数创建的,则会标识成"anonymous":
3、理解参数
ECMAScript 函数既不关心传入的参数个数,也不 关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一 个、三个,甚至一个也不传,解释器都不会报错。
之所以会这样,主要是因为 ECMAScript 函数的参数在内部表现为一个数组。
arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的 元素(第一个参数是 arguments[0],第二个参数是 arguments[1])。而要确定传进来多少个参数, 可以访问 arguments.length 属性。
可以通过 arguments[0]取得相同的参数值。因此,把函数重写成不声明参数也可以:
function sayHi() {
console.log( arguments[0] + ", " + arguments[1]);
}
sayHi('zyf','male') //zyf, male
箭头函数中的参数
如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只 能通过定义的命名参数访问。
let bar = () => {
console.log(arguments[0]);
};
bar(5); // ReferenceError: arguments is not defined
虽然箭头函数中没有 arguments 对象,但可以在包装函数中把它提供给箭头函数:
function foo() {
let bar = () => {
console.log(arguments[0]);
};
bar();
}
foo(5); // 5
4、默认参数值
function makeKing(name = 'Henry') {
return `King ${name}`;
}
console.log(makeKing()) //King Henry
暂时性锁区
function makeKing(name = 'Henry', numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
这里的默认参数会按照定义它们的顺序依次被初始化。可以依照如下示例想象一下这个过程:
function makeKing() {
let name = 'Henry';
let numerals = 'VIII';
return `King ${name} ${numerals}`;
}
因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。看下面这个例子
function makeKing(name = 'Henry', numerals = name) {
return `King ${name} ${numerals}`;
}
console.log(makeKing()); // King Henry Henry
参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。
function makeKing(name = numerals, numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
5、收集参数
function getProduct(...values, lastValue) {}
不可以这么写
function ignoreFirst(firstValue, ...values){}
可以这么写
6、函数声明与函数表达式
// 变量提升,没问题
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
// 函数表达式,会出错
console.log(sum(10, 10));
let sum = function (num1, num2) {
return num1 + num2;
};
这个函数定义包含在一个变量初始化语句中,而不是函数声明中。 这意味着代码如果没有执行到加粗的那一行,那么执行上下文中就没有函数的定义,所以上面的代码会 出错。这并不是因为使用 let 而导致的,使用 var 关键字也会碰到同样的问题:
console.log(sum(10, 10));
var sum = function(num1, num2) {
return num1 + num2;
};
7、new.target
如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的 构造函数。
function King() {
if (!new.target) {
console.log('没new啊亲');
return;
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // 没new啊亲
8、函数表达式写法
错误写法
if (condition) {
function sayHi() {
console.log('Hi!');
}
} else {
function sayHi() {
console.log('Yo!');
}
}
这种写法在 ECAMScript 中不是有效的语法。JavaScript 引擎会尝试将其纠正为适 当的声明。问题在于浏览器纠正这个问题的方式并不一致。多数浏览器会忽略 condition 直接返回第 二个声明。Firefox 会在 condition 为 true 时返回第一个声明。这种写法很危险,不要使用。不过, 如果把上面的函数声明换成函数表达式就没问题了:
正确写法:
let sayHi;
if (condition) {
sayHi = function () {
console.log("Hi!");
};
} else {
sayHi = function () {
console.log("Yo!");
};
}
9、私有变量(闭包)
特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。在对象上有 两种方式创建特权方法。第一种是在构造函数中实现,比如:
function MyObject() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function () {
privateVariable++;
return privateFunction();
};
}
所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有成员的特权方法。这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力。
变量 privateVariable 和函数 privateFunction()只能通过 publicMethod()方法来访问。在创建 MyObject 的实例后,没有办法 直接访问 privateVariable 和 privateFunction(),唯一的办法是使用 publicMethod()。
1.1 静态私有变量
(function () {
let name = '';
Person = function (value) {
name = value;
};
Person.prototype.getName = function () {
return name;
};
Person.prototype.setName = function (value) {
name = value;
};
})();
let person1 = new Person('Nicholas');
console.log(person1.getName()); // 'Nicholas'
person1.setName('Matt');
console.log(person1.getName()); // 'Matt'
let person2 = new Person('Michael');
console.log(person1.getName()); // 'Michael'
console.log(person2.getName()); // 'Michael'
不使用关键字声明的变量会创建在全局作用域中
这里的 Person 构造函数可以访问私有变量 name,跟 getName()和 setName()方法一样。使用这 种模式,name 变成了静态变量,可供所有实例使用。这意味着在任何实例上调用 setName()修改这个 变量都会影响其他实例。调用 setName()或创建新的 Person 实例都要把 name 变量设置为一个新值。 而所有实例都会返回相同的值。
像这样创建静态私有变量可以利用原型更好地重用代码,只是每个实例没有了自己的私有变量。最终,到底是把私有变量放在实例中,还是作为静态私有变量,都需要根据自己的需求来确定。
1.2 模块模式
let singleton = function () {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权/公有方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable++;
return privateFunction();
}
};
}();
模块模式使用了匿名函数返回一个对象。在匿名函数内部,首先定义私有变量和私有函数。之后, 创建一个要通过匿名函数返回的对象字面量。这个对象字面量中只包含可以公开访问的属性和方法。因 为这个对象定义在匿名函数内部,所以它的所有公有方法都可以访问同一个作用域的私有变量和私有函 数。
如果单例对象需要进行某种初始化,并且需要访 问私有变量时,那就可以采用这个模式:
let application = function () {
// 私有变量和私有函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 公共接口
return {
getComponentCount() {
return components.length;
},
registerComponent(component) {
if (typeof component == 'object') {
components.push(component);
}
}
};
}();
上面这个简单的例子创建了一个 application 对象用于管理组件。在创建这个对象之后,内部就会创建一个私有的数组 components, 然后将一个 BaseComponent 组件的新实例添加到数组中。(BaseComponent 组件的代码并不重要,在这里用它只是为了说明模块模式的用法。)对象字面量中定义的 getComponentCount()和 registerComponent()方法都是可以访问 components 私有数组的特权方法。前一个方法返回注册组件的数量, 后一个方法负责注册新组件。
另一个利用模块模式的做法是在返回对象之前先对其进行增强。这适合单例对象需要是某个特定类 型的实例,但又必须给它添加额外属性或方法的场景。来看下面的例子:
let singleton = function () {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 创建对象
function CustomType(){
}
let object = new CustomType();
// 添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function () {
privateVariable++;
return privateFunction();
};
// 返回对象
return object;
}();
console.log(singleton.publicProperty);
箭头函数里面没有构造函数constructor,function里面有
单例:
**意图:**保证一个类仅有一个实例,并提供一个访问它的全局访问点。
**主要解决:**一个全局使用的类频繁地创建与销毁。
**何时使用:**当您想控制实例数目,节省系统资源的时候。
**如何解决:**判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
var HeadClass = function () { };
var Head = (function () { // 匿名自执行函数
var instance; // 声明一个instance对象
return function () {
if (instance) { // 如果已存在 则返回instance
return instance;
}
instance = new HeadClass() // 如果不存在 则new一个HeadClass对象
return instance;
}
})();
var a = new Head();
var b = new Head();
console.log(a===b) // true
我们只需要调用new Head()即可构造一个单例模式对象,但同时也可以调用new HeadClass()构造新的对象,那么我们如何加以限制,让其只能调用new Head()来构造对象呢?
其实我们只需要把HeadClass声明放入匿名自执行函数Head内即可:
var Head = (function () {
var HeadClass = function () { }; // 声明HeadClass对象,无法在外部直接调用
var instance; // 声明一个instance对象
return function () {
if (instance) { // 如果已存在 则返回instance
return instance;
}
instance = new HeadClass() // 如果不存在 则new一个
return instance;
}
})();
var a = Head();
var b = new Head();
console.log(a===b) // true
var a = HeadClass(); // 报错,HeadClass is not defined
九、期约与异步函数
1、js中的异步和同步
同步:
严格按照顺序来执行,上一条代码执行完之后再执行
异步:
js是单线程的,按道理来说只能进行同步操作,但是因为js在浏览器中进行,而浏览器是多线程的,所以js也会具有异步的特征。比如:
setTimeout(()=>{
console.log(1);
},0)
for(let i = 0; i < 100; i++){
for(let j=0;j<100;j++){
console.log(2);
}
}
从上图打印结果可以看出即使for循环要进行很长时间,也会先输出for循环里面的值,再打印setTimeout里面的值。这是因为此时先执行setTimeout,但却把里面的函数放在了消息队列中,然后等执行完后面的代码再执行消息队列中的函数,所以可以看出,浏览器会先把代码过一遍,然后把异步代码放进消息队列中,等代码执行完再执行消息队列中的值
2、期约
1.1 期约状态
Promise有三种状态:
- 待定(pending)
- 兑现(fulfilled,有时候也称为“解决”,resolved)
- 拒绝(rejected)
待定(pending)是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现 (fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。只要从待 定转换为兑现或拒绝,期约的状态就不再改变。
let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 0, p1); // Promise <resolved>
let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2);
// Uncaught error (in promise)
// Promise <rejected>
不可逆性,如下所示:
let p = new Promise((resolve, reject) => {
resolve();
reject(); // 没有效果
});
setTimeout(console.log, 0, p); // Promise <resolved>
为避免期约卡在待定状态,可以添加一个定时退出功能。
let p = new Promise((resolve, reject) => {
setTimeout(reject, 10000); // 10 秒后调用 reject()
// 执行函数的逻辑
});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 11000, p); // Promise <rejected>
1.2 Promise.prototype.resolve()
下面两个期约实例实际上是一样的:
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
Promise.resolve()只会接收第一个参数并对其进行处理,且默认值为undefined
setTimeout(console.log, 0, Promise.resolve()); //undefind
setTimeout(console.log, 0, Promise.resolve(4, 5, 6)); //Promise <resolved>: 4
对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此, Promise.resolve()可以说是一个幂等方法:
(幂等方法是无论调用这个多少次,结果都一样)
const fff = Promise.resolve(7);
setTimeout(console.log, 0, fff === Promise.resolve(Promise.resolve(Promise.resolve(fff)))); //true
这个幂等性会保留传入期约的状态:
const p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
1.3 Promise.prototype.reject()
与 Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误 (这个错误不能通过 try/catch 捕获,而只能通过拒绝处理程序捕获,这是因为reject是异步函数,会放在消息队列里面,所以try/catch捕获不到)。
llet p1 = new Promise((resolve, reject) => reject());
console.log(p1);
console.log(1111);
//
后面的打印语句能够正常打印“1111”,理由同上
拒绝的期约的理由就是传给 Promise.reject()的第一个参数。这个参数也会传给后续的拒 绝处理程序:
let p = Promise.reject(3);
setTimeout(console.log, 0, p); // Promise <rejected>: 3
p.then(null, (e) => setTimeout(console.log, 0, e)); // 3
Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期 约对象,则这个期约会成为它返回的拒绝期约的理由:
setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
try/catch只有在同步的错误状态下才会进行捕获
try {
throw new Error('foo');
} catch(e) {
console.log(e); // Error: foo
}
try {
Promise.reject(new Error('bar'));
} catch(e) {
console.log(e);
}
直接输出了try里面的语句,把Promise.reject(new Error('bar'));当成正常情况处理了,理由同上
1.4 Promise.prototype.then()
Promise.then()返回的也是个Promise对象
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 1000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 1000));
p1.then(console.log(1111), null);
p2.then(null,console.log(2222) );
p1返回的是resolve时,它的then方法会执行第一个参数
p2返回的时reject时,它的then方法会执行第二个参数
let p1 = new Promise((resolve,reject) => {resolve(1)});
p1.then((e)=>{console.log(e)});
这里会将p1里面resolve中的参数“1”传递给p1.then里面的e
const p = new Promise((resolve, reject) => {
resolve('1111')
})
console.log(p.then());
/* 输出结果:
Promise {<pending>}
Promise 值之所以是pending,是因为此时外面new Promise中,里面的值会放在消息队列里,
而这个值resolve('1111')还处于没有状态中(pending),所以打印时,PromiseState为pendding
*/
setTimeout(() => {
console.log(p);
}, 0)
/*
输出结果:
Promise {<fulfilled>: '1111'}
Promise 值之所以是pending,是因为通过setTimeout把console.log(p)放在了消息队列中,此时因为console.log(p)和
resolve('1111')同处于消息队列中,所以可以改变Promise 的状态(resolve对应的是fulfilled)
*/
let p1 = Promise.resolve('foo');
let p2 = p1.then();
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
会根据p1的状态加载p1中的then方法,如果为resolve加载第一个参数,如果为reject加载第二个参数
let p1 = Promise.resolve('foo');
let p2 = p1.then(e=>console.log(e));
let p1 = Promise.resolve('foo');
let p3 = p1.then(null, () => undefined);
let p4 = p1.then(null, () => {});
let p5 = p1.then(null, () => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined
return和不return的区别
let p1 = Promise.reject('foo');
const p2 = p1.then(null, () => { Promise.resolve(111) });
setTimeout(console.log, 0, p2)
let p1 = Promise.reject('foo');
const p2 = p1.then(null, () => { return Promise.resolve(111) });
setTimeout(console.log, 0, p2)
上面的第一个案例为undefined是因为没有返回值,默认为undefined
let p1 = Promise.reject('foo');
let p10 = p1.then(null, () => { throw 'baz'; }); // Promise <rejected>: baz
throw 'baz'改变了状态,会默认返回Promise.reject('baz')
1.5 Promise.prototype.catch()
事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onRejected)。
const p1 = Promise.reject();
const a = ()=>111
const p2 = p1.then(null, a);
const p3 = p1.catch(a)
setTimeout(console.log,0,p2) //Promise {<fulfilled>: 111}
setTimeout(console.log,0,p3) //Promise {<fulfilled>: 111}
1.6 Promise.prototype.finally()
这个处理程序在期 约转换为解决或拒绝状态时都会执行。但 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 <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false
因为 onFinally 被设计为一个状态 无关的方法,所以在大多数情况下它将表现为父期约的传递。
let p1 = Promise.resolve('foo');
let p2 = p1.finally();
let p3 = p1.finally(() => undefined);
let p4 = p1.finally(() => {});
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => 'bar');
let p7 = p1.finally(() => Promise.resolve('bar'));
let p8 = p1.finally(() => Error('qux'));
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
setTimeout(console.log, 0, p3); // Promise <resolved>: foo
setTimeout(console.log, 0, p4); // Promise <resolved>: foo
setTimeout(console.log, 0, p5); // Promise <resolved>: foo
setTimeout(console.log, 0, p6); // Promise <resolved>: foo
setTimeout(console.log, 0, p7); // Promise <resolved>: foo
setTimeout(console.log, 0, p8); // Promise <resolved>: foo
如果返回的是一个待定的期约,或者 onFinally 处理程序抛出了错误(显式抛出或返回了一个拒 绝期约),则会返回相应的期约(待定或拒绝),如下所示:
let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
let p11 = p1.finally(() => { throw 'baz'; });
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p9); // Promise <pending>
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined
// Uncaught (in promise) baz
setTimeout(console.log, 0, p11); // Promise <rejected>: baz
1.7 期约连锁和期约合成
new Promise(
resolve => { //resolve是个函数形参数
setTimeout(() => {
resolve('hellooo'); //运行这个函数参数
}, 2000)
}
).then(
value => {
return new Promise(
resolve =>{
setTimeout(()=>{
console.log(value);
resolve(value+'world')
},1000)
}
)
}
).then(
value =>{
console.log(value+'zyf'); //helloooworldzyf
}
)
let p1 = new Promise((resolve, reject) => {
console.log('p1 executor');
setTimeout(resolve, 4000);
});
p1.then(() => new Promise((resolve, reject) => {
console.log('p2 executor');
setTimeout(resolve, 3000);
}))
.then(() => new Promise((resolve, reject) => {
console.log('p3 executor');
setTimeout(resolve, 2000);
}))
.then(() => new Promise((resolve, reject) => {
console.log('p4 executor');
}));
// p1 executor
// p2 executor
// p3 executor
// p4 executor
可以将上述写法进行个封装
function delayedResolve(str,time) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, time);
});
}
delayedResolve('p1 executor',4000)
.then(() => delayedResolve('p2 executor',3000))
.then(() => delayedResolve('p3 executor',2000))
.then(() => delayedResolve('p4 executor',1000))
因为 then()、catch()和 finally()都返回期约,所以串联这些方法也很直观。
let p = new Promise((resolve, reject) => {
console.log('initial promise rejects');
reject();
});
p.catch(() => console.log('reject handler'))
.then(() => console.log('resolve handler'))
.finally(() => console.log('finally handler'));
Promise.all
数组里面的promise全部都会执行,并返回一个数组
let p2 = Promise.all([
new Promise((resolve, reject) => setTimeout(resolve, 1000,'z')),
Promise.resolve('y'),
Promise.resolve('f')
]);
p2.then(value=>{console.log(value);})
let p2 = Promise.all([
new Promise((resolve, reject) => setTimeout(resolve, 1000,'z')),
Promise.resolve('y'),
Promise.resolve('f')
]);
p2.then(value=>{console.log(value);})
//一秒钟之后会打印出一个数组['z','y','f']
Promise.race
只会执行数组中的一个元素,谁先执行就执行谁
// 空的可迭代对象等价于 new Promise(() => {})
let p3 = Promise.race([]);
// 无效的语法
let p4 = Promise.race();
// TypeError: cannot read Symbol.iterator of undefined
let p3 = Promise.race([
Promise.resolve(5),
Promise.resolve(6),
Promise.resolve(7)
]);
setTimeout(console.log, 0, p3); // Promise <resolved>: 5
const p = new Promise((resolve,reject)=>{
reject(12222222)
})
setTimeout(console.log,0,p
.then((()=> 22222222),(e)=>{
return 111111111+e
})
.then(e=>{return e},e=>{
console.log('yf',e);
}));
then()方法中哪怕你不返回一个new Promise,它也会自动帮你将返回的值转换成Promise形式的,简单来讲只要你返回了值,你就可以继续用then方法,并且如果then方法中什么参数也不带,那么就按照用then方法之前的处理。
并且then方法中不管你是对resolve进行处理还是对reject进行处理,处理完成后就变成了resolve, 比如resolve.then(null,onreject).then(a,b),就会调用a
const p = new Promise((resolve,reject)=>{
reject(10)
})
setTimeout(console.log,0,p.then(null,val=>val+8).then(val=>{console.log(val);}))
因为最后一个then没有返回值,所以会打印出一个fulfilled:undefined
3、异步函数
使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。而在参数或闭 包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。正如下面的例子所示,foo()函数仍然会 在后面的指令之前被求值:
async function foo() {
console.log(1);
}
foo();
console.log(2);
// 1
// 2
不过,异步函数如果使用 return 关键字返回了值(如果没有 return 则会返回 undefined),这 个值会被 Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约:
async function foo() {
console.log(1);
return 3;
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 1
// 2
// 3
当然,直接返回一个期约对象也是一样的:
async function foo() {
console.log(1);
return Promise.resolve(3);
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 1
// 2
// 3
async function foo(){
await new Promise((resolve,reject)=>{
})
console.log(2); //上面的语句没有返回语句,所以这一行代码不会运行
}
foo()
async function foo(){
await new Promise((resolve,reject)=>{
reject('111')
})
console.log(2); //上一句语句报异常,这一行代码不会运行
}
foo()
async function foo() {
await new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('111')
},1000)
})
console.log('zyf'); //1秒钟过后打印'zyf'
}
foo()