个人对第四版红宝书的学习笔记。不适合小白阅读。这是part1,包含原书第二章(HTML中的Javascript)和第三章(语言基础)。持续更新,其他章节笔记看我主页。
(记 * 的表示是ES6新增的知识点,记 · 表示包含新知识点)。
新增知识点如下:let声明、const声明、模板字面量(字符串)、Symbol数据类型、for-of 循环语句。
第二章:HTML中的Javascript
2.1 <script>元素
<script>标签的八个属性
-
async
:可选。表示应该立即开始下载脚本,但不能阻止其它页面动作,比如下载资源或等待其他脚本加载。使用该属性可以异步执行脚本。只对外部脚本文件有效。 -
charset
:可选。使用src属性指定的代码字符集。基本不会使用。 -
crossorigin
:可选。配置相关的CORS(跨资源共享)设置。默认不使用CORS。corssorigin="anonymous"
配置文件请求不必设置凭据标志。corssorigin="use-credentials"
设置凭据标志,意味着出战请求会包含凭据。 -
defer
:可选。表示在文档解析和显示完成后再执行脚本是没有问题的。只对外部脚本文件有效。IE7及以前可以对行内脚本指定该属性。使用该属性推迟执行脚本。 -
integrity
:可选。允许对比接收到的资源和指定的加密签名以验证子资源完整性(SRI)。如果接收到资源的签名与这个属性指定的签名不匹配,则页面会报错,脚本不会执行。该属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容。 -
language
:已废弃。 -
src
:可选。表示包含要执行的代码的外部文件。 -
type
:可选。代替language,表示代码块中脚本语言的内容类型(也称MIME类型)。按惯例始终都会是text/javascript
。Javascript文件的MIME类型通常是"application/x-javascript"
,不过给type属性这个值有可能导致脚本被忽略。在非IE浏览器有效的其他值还有"application/javascript"
和"application/ecmascript"
。如果这个值是module,则代码会被当成ES6模块,且只有这时候代码中才能出现import和export关键字。
三个注意点:
-
使用<script>标签写行内代码时不要出现这种情况:
<script> console.log("</script>"); <!--这里</script>会被当作结束标签,甚至你会发现写在注释中也会被当作结束标签无法解析,因此我这里用了html的注释法--> </script>
可以使用转义字符解决这一问题:
<script> console.log("<\/script>"); //这里可以正常使用,注释里这样使用:<\/script> 也可以,从代码没有高亮可以看出来 </script>
-
该元素最为强大同时也备受争议的特性是:它可以包含来自外部域的Javascript文件。它的src属性可以是一个完整的url,而且这个url指向的资源可以跟包含它的HTML页面不再同一个域中。例如:
<script src="http://www.somewhere.com/afile.js"></script>
浏览器解析时会向src属性指定的路径发送一个GET请求,以取得相应资源。这个初始的请求不受浏览器同源策略限制,但返回的Javascript受限。(当然,该请求仍然受HTTP/HTTPS)协议的限制。
来自外部的代码会被当成加载他它的页面的一部分来加载和解析。**这个能力可以让我们通过不同的域分发JavaScript。**不过同时,引用他人服务器的文件时必须格外小心,因为可能会有恶意的程序员替换这个文件。integrity属性可以防范,但不是所有浏览器都支持。
-
一般浏览器会按照<script>在页面中的顺序依次解释它们,只要没有使用defer和async属性的话。另外最好将标签位置放在页面底部(<body>之后)。
###2.2 行内代码与外部文件
虽然不是明确的强制性规则,但通常认为最佳实践是尽可能将Javascript代码放在外部文件中。原因如下:
- 可维护性。使用一个目录保存所有的JavaScript文件总会比分散在很多HTML页面容易维护。
- 缓存。浏览器会根据特点的设置缓存所有外部链接的JavaScript文件,这意味着如果两个页面都用到同一个文件,则该文件只需被下载依次。
- 适应未来。
2.3 文档模式
IE5.5发明了文档模式的概念,即可以通过doctype切换文档模式。最初的文档模式有两种:混杂模式(quirks mode)和标准模式(standards mode)。后来出现了第三种文档模式:准文档模式(almost standards mode)。只作了解。
2.4 <noscript>元素
针对早期浏览器不支持JavaScript的问题,提出的一个页面优雅降级的处理方案:
<noscript>
<p>该页面不支持JavaScript,请更换浏览器。</p>
</noscript>
<noscript>元素可以包含任何出现在<body>中的HTML元素,<script>除外。出现下列两种情况下,浏览器将显示包含在该元素中的内容:
- 浏览器不支持脚本;
- 浏览器对脚本的支持被关闭。
第三章:语言基础
3.1 变量声明
3.1.1 var声明
var声明的变量不初始化的情况下,该变量会保存特殊的值undefined。
var声明厨初始化后的变量,后续可以改变类型。
1)var声明作用域
使用var操作符定义的变量会成为包含它的函数的局部变量,该变量在函数退出时被销毁。而在局部作用域中省略var操作符声明,该变量会作为全局变量:
function test(){
var m1="hi";
m2="hi";
}
test();
console.log(m1); //-> 出错!
console.log(m2); //-> hi
2)var声明提升
使用var关键字时,声明的变量会自动提升(hoist)到函数作用域顶部。此外,反复多次使用var声明同一个变量也没有问题。
function test(){
console.log(age);
var age=18;
}
/*上述代码在ECMAScript中运行时会看成等价如下代码:*/
function test(){
var age;
console.log(age);
age=18;
}
//因此调用该方法,结果会如下:
test(); //-> undefined
3.1.2 let声明 *
let和var差不多,但有着很大的区别,最明显的区别是:let声明的范围是块作用域,而var是函数作用域。
if(true){
var name1='matt';
}
console.log(name1); //-> matt
if(true){
let name2='mat';
}
console.log(name2); //-> ReferenceError:name没有定义
这里name2之所以不能再if块外部被引用,是因为它的作用域仅限于该块内部。块作用域是函数作用域的子集,因此适用于var的作用域限制同样适用于let。
let也不允许同一个块作用域中出现冗余声明。
var age;
var age;
let age;
let age; //-> SyntaxError; 标识符age已经声明过了
对声明冗余报错不会因混用let和var而受影响。它们声明的不是不同类型的变量,只是指出变量在相关作用域如今存在。
var name;
let name; //-> SyntaxError
let age;
var age; //-> SyntaxError
1)暂时性死区
let和var另一个重要的区别,就是let声明的变量不会再作用域中被提升。
在let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError。
2)全局声明
let在全局作用域中声明的变量不会成为window 对象的属性(var声明的变量则会)。不过let声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。
var name='matt';
console.log(window.name); //-> 'matt'
let age=18;
console.log(window.age); //-> undefined
3)条件声明
使用var声明时,由于声明会被提升,JavaScript引擎会自动将多余的声明在作用域顶部合并为一个声明。因为let的作用域是块,所以不可能检查前面是否已经使用let声明过同名变量,同时也就不可能在没有声明的情况下声明它。
4)for循环中的let声明
在let出现之前,for循环定义的迭代变量会渗透到循环体外部:
for(var i=0;i<5;i++){
//循环体
}
console.log(i); //-> 5
而let就解决了这一问题,因为迭代遍历的作用域仅限于for循环块内部。
for(let i=0;i<5;i++){
//循环体
}
console.log(i); //-> ReferenceError:i没有定义
另外在使用var的时候,最常见的问题就是对迭代变量的奇特声明和修改:
for(var i=0;i<5;i++){
setTimeout(() => console.log(i),0); //-> 5、5、5、5、5
}
//因为在退出循环时,迭代变量保存的都是导致循环退出的值:5。之后执行超时逻辑时,所有的i都是同一个变量。
而在使用let声明迭代变量时,JavaScript引擎在后台会为每个迭代循环声明一个新的迭代变量。
for(let i=0;i<5;i++){
setTimeout(() => console.log(i),0); //-> 1、2、3、4、5
}
这种行为适用于于所有风格的for循环,包括for-in和for-of循环。
3.1.3 const声明 *
const的行为与let基本相同,唯一一个重要的区别是用它声明变量是时必须同时初始化变量,且后续尝试修改const变量会报错。
const声明的限制只适用于它指向的变量的引用。换言之,如果const变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制。
另外const不能用来声明迭代变量(因为迭代变量会自增或自减)。但const可以用来声明一个不会被修改的for循环变量,也就是说,每次迭代只是创建一个新变量。
let i=0;
for(const j=7;i<5;++i){
console.log(j); //-> 7,7,7,7,7
}
for(const key in {a:1,b:2}){
console.log(key); //-> a,b
}
for(const value of [1,2,3,4,5]){
console.log(value); //-> 1,2,3,4,5
}
//可以看出,这对for-of和for-in循环特别有意义
3.2 数据类型
3.2.1 typeof操作符
使用typeof操作符检验数据类型。可以用来区分函数和对象。
alert(typeof 95);
alert(typeof(95)); //看的出来typeof操作符也可以使用参数
alert(typeof null); //会返回object,因为特殊值null被认为是一个对空对象的引用
3.2.2 Undefined类型
在声明变量但未对其加以初始化时,这个变量的值就是undefined。
而当使用typeof操作符检验一个未声明的变量时,返回的值也是undefined。
3.2.3 Null类型
逻辑上,null值表示一个空对象指针。所以使用typeof检验null会返回object。
ECMA-262规定 undefined==null 返回 true。
无论什么情况都不需要将变量值显式地设置undefined,但对null不适用。换言之,只要意在保存的对象还没有真正保存对象,就应该明确地让该变量保存null。
3.2.4 Boolean类型
true、false是区分大小写的。
布尔类型转换Boolean()
使用**Boolean()**函数将对应的值转化为布尔值。下面是转化规则:
数据类型 | 转化为true的值 | 转化为false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | 空字符串 |
Number | 任何非零数字值(包括无穷值) | 0和NaN |
Object | 任何对象 | null |
Undefined | n/a(不适用) | undefined |
3.2.5 Numer类型
除了十进制表示以外,整数还可以通过八进制或十六进制的字面值表示。
其中,八进制字面值的第一位必须是零(0)。如果字面值中的数值超出了范围,那么前导零将被忽略,后面的数值将被当作十进制数值解析。注意,八进制字面量在严格模式下是无效的,会导致支持的Js引擎抛出错误。
十六进制字面值的前两位必须是0x,后跟任何十六进制数(09及AF)。其中,字母A~F可以大写,也可以小写。
进行算术计算时,所有的八进制和十六进制数最终都将被转化为十进制数。
1)浮点数
由于保存浮点数值需要的内存空间是整数的两倍,因此ECMAScript会不失时机地将浮点数值转化为整数值。如果小数点后没有跟任何数字(如 1.)或浮点数值本身表示的就是一个整数(如1.0),就会将其转化为整数。
默认情况下,ECMAScript会将那些小数点后面带有6个0以上的浮点数值转化为以e表示法表示的数值。
浮点数值的最高精度是17位小数,但在进行算术计算时其精确度远不如整数。例如,0.1+0.2的结果不是0.3,而是0.30000000000000004(小数点后一共17位)。这个舍入误差会导致无法测试指定的浮点数值。例如:
if(a + b == 0.3){ //不要做这样的测试!
alert("You get 0.3");
}
//在这个例子中,我们测试的是两个数的和是不是等于0.3。若这两个数是0.05和0.25,或者是0.15和0.15都不会有问题。因此,永远不要测试某个特定的浮点数
2)数值范围
由于内存限制,ECMAScript无法保存世界上所有数据。ECMAScript能够表示的最小数值保存在Number.MIN_VALUE
中——在大多数浏览器中,这个值是5e-324;能够表示的最大数值保存在Number.MAX_VALUE
中——在大多数浏览器中,这个值是1.7976931348623157e+308。如果这个数值是正数,则会转化为**Infinity
**。
要想确定一个数值是不是有穷的(换言之,是不是位于最小和最大数值之间),可以使用isFinite()
函数。这个函数在参数位于最小与最大数值之间会返回true。
3)NaN
NaN,即非数值(Not a Number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况(这样不会抛出错误)。例如在其他编程语言中,任何数除以0都会导致错误,但在ECMAScript中会返回NaN,因此不会影响其他代码执行。
NaN有两个特点。首先,任何涉及NaN的操作都会返回NaN。其次,NaN与任何值都不相等,包括NaN本身。
针对NaN,ECMAScript定义了一个函数isNaN()
函数。这个函数接受一个参数(可以是任意类型),函数会帮我们确定该参数是否“不是数值”。函数在接受到值后,会尝试将该值转化为数值,某些不是数值的值会直接转化为数值,例如字符串"10"或Boolean值。任何不能被转化为数值的值都会导致函数返回true。
alert(isNaN(NaN)); //true
alert(isNan(10)); //false(10是一个数值)
alert(isNan("10")); //false(会转化为数值10)
alert(isNan("blue")); //true(无法转化为数值)
alert(isNan(true)); //false(可以被转化为数值1)
而对于isNaN()
,它也适用于对象。在基于对象调用该函数时,会首先调用对象的valueOf()
方法,然后确定该方法返回的值是否可以转化为数值。如果不能,则基于这个返回值再调用 toString()
方法,再测试返回值。
4)数值转换
有三个函数可以将非数值转化为数值:Number()
、parseInt()
和parseFloat()
。第一个转型函数Number()
可以用于任何数据类型,而另两个函数则专门用于将字符串转换成数值。
Number()函数
-
如果是布尔值,true和false将分别转换为1和0。
-
如果数字值,只是简单的传入与返回。
-
如果是null,返回0。
-
如果是undefined,返回NaN。
-
如果是字符串,遵循下列规则:
- 如果字符串中只包含数字,则将其转化为十进制,如"12"会转化为12,"0123"转化为123(注意,前导的零被忽略了)。
- 如果字符串中包含有效的浮点格式,如"1.1",则将其转化为对应的浮点数值(同样会忽略前导零)
- 如果字符串中包含有效的十六进制格式,则将其转化为相同大小的十进制整数;
- 如果为空字符串,转化为0;
- 如果字符串包含上述格式之外的字符,则转换为NaN。
-
如果是对象,则调用对象的valueOf()方法,然后依照前面的规则转换返回的值。如果结果是NaN,则调用对象的toString()方法,然后再次依照前面的规则转换返回的字符串值。
ParseInt()函数
由于Number()
函数在转化时比较复杂且不够合理,因此在处理整数的时候更常用的是parseInt()
函数。该函数会忽略字符串前的空格,直至找到第一个非空格字符。如果第一个字符不是数字或者负号,函数就会返回NaN;也就是说,parseInt()函数对空字符串会返回NaN(而Number()函数会返回0)。如果第一个字符是数字字符,则会继续解析第二个字符直到全部解析完毕或者遇到了第一个非数字字符。
如果字符串中的第一个字符是数字字符,parseInt()也能识别各种整数格式。也就是说,如果字符串以"0x"开头且后跟数字字符,就会将其当作十六进制整数,如果字符串以"0"开头且后跟数字字符,则会将其解析为八进制数。
【注】对于八进制数如 070 ,ES3和ES5存在分歧,ES3会解析为56,而ES5会解析为0。在ES5 JS引擎中,parseInt()函数已不再具备解析八进制的能力,因此前导零无效,解析为0。严格模式下同样如此。
【续】为消除可能产生的困惑,可以为这个函数提供第二个参数:转换时使用的基数(即多少进制)。例如:
var num=parseInt("0xAF",16); //175
//而实际上如果指定了16进制,字符串可以不带前面的0x。
var num=parseInt("AF"); //NaN
var num=parseInt("AF",16); //175
//指定基数会影响转换的输出结果
var num1=parseInt("10",2); //2
var num2=parseInt("10",8); //8
为了避免解析的错误,建议无论在什么情况下都明确指定基数。
parseFloat()函数
该函数同parseInt()
函数类似,也是从第一个字符位置开始解析每个字符,同样解析到字符串结尾,或者解析到遇见一个无效的浮点数字符为止。也就是说,字符串中的第一个小数点是有效的,而第二个小数点就无效了。
除第一个小数点有效之外,parseFloat()
函数与parseInt()
的第二个区别在于它始终都会忽略前导的零。parseFloat()函数对于十六进制格式字符串则始终会转换成0。parseFloat()没有指定进制第二参数的用法。且若字符串包含的是一个可解析为整数的数(没有小数点或者小数点后都为0),则会返回整数。
3.2.6 String类型
字符串的表达方式:可以使用双引号("")、单引号(’’)和反引号(``)表示。
1)字符字面量(转义序列)
字面量 | 含义 |
---|---|
\n | 换行 |
\t | 制表 |
\b | 空格 |
\r | 回车 |
\f | 换页 |
\` \" \’ | 字符串标志符号 |
\xnn | 以十六进制编码nn表示的一个字符(其中n为0~F) |
\unnnn | 以十六进制编码nnnn表示的一个Unicode字符 |
**一个转义序列表示一个字符。**字符串的长度可以使用length属性获取。
如果字符串中包含双字节字符,那么length属性返回的值可能不是准确的字符数。第五章会具体讨论如何解决这个问题。
2)字符串的特点
ES中的字符串不可变。字符串一旦创建,它们的值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充改变量。
3)转换为字符串
toString()
要将一个值转换为字符串有两种方式。第一种是使用几乎每个值都有的toString()
方法。数值、布尔值、对象和字符串值(字符串调用该方法返回字符串的一个副本)都有该方法。但null和undefined值没有该方法。
多数情况下,调用toString()
方法不必传递参数。但是,在调用数值的toString()方式时,可以传递一个参数:输出数值的基数。默认情况,该方法以十进制格式返回数值的字符串表示。通过传递基数,可以输出其他任意有效进制格式的表示。
String()
在不知道要转换的值是不是null或undefined的情况下,可以使用String()
方法,这个函数能够将任何类型的值转化为字符串。该方法遵循下列转换规则:
- 如果值有toString()方法,则调用该方法(无参数)并返回相应结果;
- 如果值为null,则返回"null";
- 如果值为undefined,则返回"undefined"。
4)模板字面量 *
ES6新增了使用模板字面量定义字符串的能力。与使用单引号和双引号不同,模板字面量保留换行字符,可以跨行定义字符串:
let str1='first line\nsecond line';
let str2=`first line
second line`;
console.log(str1);
/*-> first line
second line */
console.log(str2);
/*-> first line
second line */
console.log(str1===str2); //-> true
顾名思义,模板字面量在定义模板时特别有用。如下html模板:
let pageHTML=`
<div>
<a href="#">
<span>Jake</span>
</a>
</div>
`;
//这里可以注意,这里字符串其实是以换行符开始的。如果打印
console.log(pageHTML[0]==='\n'); //-> 结果会是true
但同时,因为模板字面量会保持反引号内部的空格,因此使用时需格外小心。(这些空格也算一个字符)
5)字符串插值 *
模板字面量最常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。技术上来说,模板字面量不是字符串,而是一种特殊的Javascript句法表达式,只不过求值之后得到的是字符串。模板字面量在定义时立即求值并转化为字符串实例,任何插入的变量也会从它们最近的作用域中取值。
使用${}
实现字符串插值:
let name='Jack',
age=18;
let str=`My name is ${name}, I'm ${age} years old`;
所有插入的值都会使用toString()
强制转型为字符串,任何JS表达式都可以用于插值(也就是说函数和方法也可以)。嵌套的模板字符串无需转义:
console.log('Hello, ${'world'} !'); //-> Hello, world!
此外,模板也可以插入自己之前的值:
let val='';
function append(){
val=`${val}abc`;
console.log(val);
}
append(); //-> abc
append(); //-> abcabc
append(); //-> abcabcabc
6)模板字面量标签函数 *
模板字面量也支持定义标签函数(tag function),通过标签函数可以自定义插值行为。标签函数会接受被插值记号分隔后的模板和对每个表达式求值的结果。
标签函数本身是一个常规函数,通过前缀到字面量来应用自定义行为,如下所示。标签函数接收到的参数依次是原始字符串数组和对每个表达式求值的结果。这个函数的返回值是对模板字面量求值得到的字符串。
let a=6,
b=9;
function simpleTag(strings,aValExression,bValExression,sumValExpression){
console.log(strings);
console.log(aValExression);
console.log(bValExression);
console.log(sumValExpression);
return 'foobar';
}
let untaggedResult=`${a} + ${b} = ${a+b}`;
let taggedResult=simpleTag`${a} + ${b} = ${a+b}`;
// ["", " + ", " = ", ""] 这里是插值未生效的原始字符串数组
// 6 这里是第一个插值表达式的结果,也就是 a = 6
//9 第二个插值表达式的结果,也就是 b = 9
//15 第三个插值表达式的结果,也就是 a+b = 15
console.log(untaggedResult); //-> "6 + 9 = 15"
console.log(taggedResult); //-> "foobar"
因为表达式的参数的数量是可变的,所以通常应该使用剩余操作符(rest operator)将它们收集到数组中:
let a=6,
b=9;
function simpleTag(strings,...expressions){
console.log(strings);
for(const exp of expressions){
console.log(exp);
}
return 'foobar';
}
//调用结果同上,不赘述
对于有n个插值的模板字面量。传给标签函数的表达式参数个数始终是n,加上第一个参数则传给标签函数的参数始终是n+1。因此,如果想把这些字符串和对表达式求值的结果拼接起来作为默认返回的字符串,可以这样做:
let a=6,
b=9;
function zipTag(strings,...expressions){
return strings[0] +
expressions.map((e,i) => `${e}${strings[i+1]}`)
.join('');
//map():参数1表示当前元素的值;参数2表示当前元素的索引值
//join():按照给定的字符串作为分隔符拼接整个数组
//拼接思路:先将原始字符串数组的第一个元素单独拿出来;将保存插值表达式结果的数组用map遍历,返回的值为 “当前插值表达式结果” + “对应的下一个原始数组字符串” 所产生的表达式,最后用join拼接。
/*例子的拼接:第一个原始字符串元素:"" ;
插值表达式数组:第一次遍历:6 + " + "; -> 返回 "6 + "
第二次遍历:9 + " = "; -> 返回 "9 + "
第三次遍历:15 + ""; -> 返回 "15"
join拼接:"6 + 9 = 15"
*/
}
let untaggedResult=`${a} + ${b} = ${a+b}`;
let taggedResult=zipTag`${a} + ${b} = ${a+b}`;
console.log(untaggedResult); //-> "6 + 9 = 15"
console.log(taggedResult); //-> "6 + 9 = 15"
7)原始字符串 *
使用模板字面量也可以直接获取原始的模板字面量内容(如换行符和Unicode字符),而不是被转换后的字符表示。为此,可以使用默认的String.raw标签函数:
console.log(`\u00A9`); //-> © 对应的Unicode字符:版权符
console.log(String.raw`\u00A9`); //-> \u00A9
注意:原字符串中自带转义序列如换行符,可以直接获取到。但是对实际的换行行为无用,它们不会被转换成转义序列的形式。
另外,可以通过标签函数的第一个参数(即字符串数组)的**.raw属性**取得每个字符串的原始内容。
function printRaw(strings){
for(const rawStr of strings.raw)
console.log(rawStr);
}
printRaw`\u00A9 ${'and'} \n`;
//-> \u00A9 返回的是原始内容,而非对应的Unicode字符
//-> \n
3.2.7 symbol类型 *
Symbol(符号)是ES6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
1)符号的基本用法 *
符号需要使用Symbol()
函数初始化。typeof
操作符返回symobol:
let sym=Symbol();
console.log(typeof sym); //-> symbol
可以传入一个字符串参数作为对符号的描述。符号没有字面量语法。
Symbol()
函数不能用作构造函数,与new关键字一起使用。这样避免创建符号包装对象,像使用Boolean、String、Number一样。如果确实想使用符号包装对象,可以借用Object函数。
let sym=new Symbol();
console.log(sym);//-> TypeError: Symbol is not a constructor
let mySym=Symbol();
let myWrappedSym=Object(mySym); //使用Object()创建符号包装对象
console.log(typeof myWrappedSym); //-> object
2)使用全局符号注册表 *
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。使用Symbol.for()
函数。
Symbol.for()
对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同的字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
let fooGlobalSymbol=Symbol.for('foo'); //创建新符号
let otherFooGlobalSymbol=symbol.for('foo'); //重用已有符号
console.log(fooGlobalSymbol===otherFooGlobalSymbol);//->true
//但是要注意,即便采用相同的符号描述,在全局注册表中定义的符号和使用SYmbol()定义的符号也并不等同:
let localSymbol=Symbol('foo');
console.log(localSymbol===fooGlobalSymbol); //-> false
全局注册表中的符号必须使用字符串键来创建,因此传给Symbol.for()
的任何值都会被转换为字符串。注册表中使用的键也会同时被用作符号描述。
还可以使用Symbol.keyFor()
来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。若查询的不是全局符号,则返回undefined。若查询的不是符号,则会抛出TypeError。
3)使用符号作为属性 *
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()
/ object.definedProperties()
定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]:'foo val'
} //也可以这样写:o[s1]=‘foo val';
console.log(o); //-> { Symbol(foo): foo val }
Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o);
//-> {Symbol(foo): foo val, Symbol(bar): bar val}
Object,defineProperties(o,{
[s3]:{value:'baz val'},
[s4):{value:'qux val'}
});
console.log (o):
/* -> {Symbol(foo): foo val, Symbol(bar): bar val,
Symbol(baz): baz val, Symbol(qux): qux val} */
object.getOwnPropertyNames()
返回对象实例的常规属性数组,而Object.getOwnPropertySymbols()
返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()
会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()
会返回两种类型的键:
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
}
console.log (Object.getOwnPropertySymbols(o));
//-> [symbol(foo),Symbol(bar)] 只返回符号属性数组
console.log(Object.getOwnPropertyNames(o));
//-> ["baz","qux"] 只返回常规属性数组,与上互斥
console.log(Object.getOwnPropertyDescriptors (o));
//-> {baz: (...), qux: (...), Symbol(foo): (...), Symbol (bar):(...)} 常规属性和符号属性都返回了
console.log(Reflect.ownkeys(o));
//-> ["baz",“qux”,Symbol(foo),Symbol (bar)] 返回的是常规属性和符号属性两种的键
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
//和上面不同,这里直接在对象中使用Symbol()创建了符号实例作为属性,而没有显式的保存这些实例
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
}
console.log(o);
//-> (Symbol(foo):'foo val', Symbol(bar): 'bar val')
let barSymbol = Object.getOwnPropertySymbols(o)
.find((symbol) => symbol.tostring().match(/bar/));
congole.log(barSymbol); //-> Symbol(bar)
4)常用内置符号 *
ECMAScript 6 也引入了一批常用内置符号(well-known symbol ),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of 循环会在相关对象上使用Symbol.iterator
属性,那么就可以通过在自定义对象上重新定义Symbol.iterator
的值,来改变for-of在迭代该对象时的行为。
这些内置符号也没有什么特别之处,它们就是全局函数symbol的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
注意:在提到ECMAScript规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@giterator 指的就是Symbol.iterator。
PS:后续一些ES6内置的Symbol值,也即是常用内置符号,将不在此提及。这里有篇CSDN上简单的总结:JavaScriptES6内置的Symbol值。(文章缺少书中提及的另一个内置符号:Symbol.asyncIterator。但是由于该属性是ES2018规范的,因此只有版本非常新的浏览器才支持,所以也没必要全了解。用到的话就百度吧。)
3.2.8 Obejct类型
使用 new Object()
新建一个对象。(可以省略括号,但不推荐)
Obeject类型的每个实例都具有下列属性和方法:
constructor
:用于创建当前对象的函数。hasOwnProperty(propertyName)
:用于判断当前对象实例中(不是原型)是否存在给定的属性。其中,作为参数的属性名必须以字符串形式存在(例如:o.hasOwnProperty("name")
)。isPrototypeOf(object)
:用于检查传入的对象是否是另一个对象的原型。propertyIsEnumerable(*propertyName*)
:用于检查给定的属性是否能够使用for-in语句来枚举。参数必须以字符串形式存在。toLocalString()
:返回对象的字符串表示,该字符串与执行环境的地区对应。toString()
:返回对象的字符串表示。valueOf()
:返回对象的字符串、数值或布尔值表示。通常与toString()
方法的返回值相同。
3.3 操作符
3.3.1 一元操作符
1)递增递减操作符 ++ / –
递增递减操作符直接照搬自C语言,且分为前置型和后置型。
使用前置型时,变量的值都是在语句被求值以前改变的**。且由于前置递增和递减操作与执行语句的优先级相等,因此整个语句会从左至右被求值。例如:
var num1 = 2;
var num2 = 20;
var num3 = --num1 + num2; //21
var num4 = num1 + num2; //21
后置型递增和递减操作都是在包含它们的语句被求值后才执行的。
var num1 = 2;
var num2 = 20;
var num3 = num1-- + num2; //22,此时--还未执行
var num4 = num1 + num2; //21,使用了num1递减后的值
这些操作符适用任何类型的值。在应用不同的值时,遵循下列规则:
- 应用一个包含有效数字字符的字符串时,先将其转化为数字值,再执行加减1的操作。字符串变量变成数值变量。
- 应用一个不包含有效数字的字符串时,将变量的值设置为NaN。字符串变量变为数值变量。
- 应用布尔值false时,先将其转化为0再执行加减1的操作。布尔值变量变为数值变量。
- 应用布尔值true时,先将其转化为1再执行加减1的操作。布尔值变量变为数值变量。
- 应用于对象时,(后面第5章会详细介绍)先调用对象的
valueOf()
方法以取得一个可供操作的值。然后对该值应用前述规则。如果结果是NaN,则调用toString()
方法后再应用前述规则。对象变量变成数值变量。
2)一元加减操作符 + / -
一元加操作符以一个加号表示,放在数值前不会产生任何影响。但应用在非数值时,该操作符会像Number()
转型函数一样对这个值进行转换。
一元减操作符应用于数值时,该值会变成负数。应用于非数值时,遵循与一元加操作符相同的规则,最后将值转化为负数。
3.3.2 位操作符
位操作符用于数值的底层操作,即按内存中表示数值的位来操作数值。位操作符并不直接操作64位的值,而是先将64位转化为32位的整数,然后执行操作,最后再将结果转换回64位。
对于有符号的整数,32位中的前31位表示整数的值。第三十二位(即符号位)表示数值的符号:0表示正数,1表示负数。正数以纯二进制格式存储。
负数同样以二进制码存储,但使用的格式是二进制补码。计算补码步骤:
- 求这个数值绝对值的二进制码;
- 求二进制反码,即0替换为1,1替换为0;
- 得到的二进制反码加1。
1)按位非(NOT)
按位非操作符由一个波浪线(~)表示,执行按位非的结果就是返回数值的反码。
var num1 = 25; //二进制00000000000000000000000000011001
var num2 = ~num1; //二进制1111111111111111111111111100110
alert(num2); //-26
按位非操作的本质:操作数的负值减1。但相比负值减1的操作,由于按位非是在数值表示的最底层执行操作,因此操作速度更快。
2)按位与(AND)
按位与操作符由一个和号字符(&)表示,它有两个操作符数。从本质上讲,按位与操作就是将两个数值的每一位对齐,然后根据***对应位都是1时才返回1,任何一位是0,结果都是0***的规则,对相同位置上的两个数执行AND操作。例如:
var result = 25 & 3;
alert(result); //1
//底层操作:
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
AND = 0000 0000 0000 0000 0000 0000 0000 0001
3)按位或(OR)
按位或操作符由一个竖线符号(|)表示。同样也有两个操作数。根据***有一个位是1的情况下就返回1,只有两个都是0的情况下才返回0***的规则执行OR操作。
var result = 25 | 3;
alert(result); //27
4)按位异或(XOR)
按位异或由一个插入符号(^)表示。也有两个操作数。遵循两个数值***对应位上只有一个1时才返回1,如果对应的两位都是1或都是0,则返回0***的规则。
5)左移
左移操作符由两个小于号(<<)表示,这个操作符会将数值的所有位向左移动指定的位数。例如:
var oldValue = 2; //二进制码10
var newValue = oldValue << 5; //二进制码1000000
//向左位移后,原数值的右侧多出了5个空位,左移操作会以0填充这些空位。
注意:左移不会影响操作数的符号位。换言之,-2左移5位的结果是-64而非64。
6)右移
有符号的右移操作符由两个大于号(>>)表示。这个操作符会将数值向右移动5位,但保留符号位(即正负号标记)。
同样,在移位过程中,原数值也会出现空位,而这次的空位出现在原数值的左侧、符号位的右侧。而此时ECMAScript会用符号位的值来填充所有的空位。
7)无符号右移
无符号右移操作符以三个大于号(>>>)表示。这个操作符会将数值的所有32位都向右移动。对正数来说,无符号右移与有符号右移相同。
对于负数,无符号右移是以0填充空位而非以符号位的值。其次,无符号右移操作符会把负数的二进制码当成正数的二进制码。由于负数以其绝对值的二进制补码形式表示,因此会导致无符号右移后的结果非常大。例如:
var oldValue = -64;//等于二进制111111111111111111111111000000
var newValue = oldValue >>> 5; //等于十进制134217726
//这里无符号右移操作符会将这个二进制码当成正数的二进制码,换算成十进制就是4294967232,将其右移5位,结果就变成了000001111111111111111111111110,即十进制的134217726。
3.3.3 布尔操作符
1)逻辑非
逻辑非操作符由一个感叹号(!)表示。无论这个值是什么数据类型,这个操作符都会返回一个布尔值然后对其求反。
同时使用两个逻辑非操作符,实际上就会模拟Boolean()转型函数的行为。
2)逻辑与
逻辑与操作符由两个和号(&&)表示。逻辑与可以应用在任何类型的操作数,在有一个操作数不是布尔值的情况下,遵循下列规则:
- 第一个操作数是对象,此时返回第二个操作数;
- 第二个操作数是对象,则只有在第一个操作数的求值结果位是true的情况下才会返回该对象;
- 如果两个操作数都是对象,则返回第二个操作数;
- 如果有一个操作数是null / NaN / undefined ,则返回null / NaN / undefined 。
逻辑与操作属于短路操作。即若第一个操作数求值结果为false,就不会对第二个数进行求值了。
3)逻辑或
逻辑或操作符由两个竖线符号(||)表示。逻辑或在有一个操作数不是布尔值的情况下遵循下列规则:
- 第一个操作数是对象,则返回第一个操作数;
- 第一个操作数的求值结果为false,则返回第二个操作数;
- 如果两个操作数都是对象,则返回第一个对象
- 如果两个操作数都是null / NaN / undefined ,则返回null / NaN / undefined 。
**逻辑或同属短路操作。**即若第一个操作数求值结果为true,就不会对第二个数进行求值了。
我们可以利用逻辑或这一行为来避免为变量赋null或undefined值。例如:
var myObject = preferredObject || backupObject;
//在这个例子中,变量myObject将被赋予等号后面两个值中一个。变量preferredObject中包含优先赋给变量myObject的值,变量backupObject负责在preferredObject中不包含有效值的情况下提供后备值。
PS:后面还有乘性操作符、加性操作符、关系操作符、相等操作符、赋值操作符、逗号操作符,就不再赘述。只要知道:
乘性操作符、加性操作符、关系操作符在操作数为非数值的情况下,执行运算时都可以在后台转换不同的数据类型。
相等操作符:简单来说:相等( == )和不相等( != )操作符在操作数类型不同时会先进行强制类型转换再比较;而全等( === )和全不等( !== )仅作比较而不会转换。当然,涉及到对象的时候会复杂点,但是这里也没必要多做讨论。
在赋值时使用逗号操作符分隔值,最终会返回表达式中的最后一个值(这种使用场景并不多见,但确实存在):
let num = (5,1,2,3,0); //-> num的值会是0
3.4 语句
if、for、while、do-while、break、continue、switch语句这里不再提及。
因为不推荐with语句,所以这里也不再提及。with语句在严格模式下会报错。
3.4.1 循环语句 `
for-in语句
语法如下:
for(property in expression) statement
-
定义迭代变量时推荐使用const(就如之前使用一样);
-
for-in不能保证返回对象属性的顺序;
-
如果要迭代的变量是null和undefined。则不执行循环体。
for-of语句
语法如下:
for(property of expression) statement
- 定义迭代变量推荐使用const;
- for-of循环会按照可迭代对象的
next()
方法产生值得顺序迭代元素。可迭代对象会在第7章介绍。 - 若尝试迭代变量的不支持迭代,则语句会抛出错误。
注:ES2018对for-of语句,增加了for-await-of 循环,以支持生成期约(promise)的异步可迭代对象。(这个新增循环和前面提到的常用内置符号
Symbol.asyncIterator
有关系,可以自行了解)
3.4.2 标签语句
使用label语句可以在代码中添加标签,以便将来使用。语法:
label : statement
//实例:
start : for (var i = 0; i < count; i++){
alert(i);
}
//该例子中定义的start标签可以在将来由break或continue语句引用。加标签的语句一般都要与for语句等循环语句配合使用。
3.5 函数
函数体中语句碰到return语句会立即停止执行并退出,后续代码不会被执行。return语句也可以不带返回值。这时候,函数会立即停止并返回undefined。这种用法最常用于提前终止函数执行。
严格模式下对函数有一些限制,若发生以下情况,会发生语法错误:
- 不能把函数命名为eval或arguments;
- 不能把函数的参数命名为eval或arguments;
- 不能出现两个命名参数同名的情况。
第10章会更详细的介绍函数。