javascript高级程序设计(第四版)学习笔录


(26-第一章20220217+a0732

JavaScript的组成

完整的javascript分为三部分:

  • 核心:ECMAScript
  • 文档对象模型:DOM
  • 浏览器对象模型:BOM

ECMAScript

ECMAScript即ECMA-262定义的语言;

Web浏览器、Node.js和Adobe Flash等只是ECMAScript实现可能存在的一种宿主环境(host environment);

宿主环境提供ECMAScript的基准实现与环境自身交互必须的扩展;

如果不涉及浏览器,ECMA-262再基本的层面,它描述这门语言的:

  • 语法
  • 类型
  • 语句
  • 关键字
  • 保留字
  • 操作符
  • 全局对象

DOM

文档对象模型(DOM,Document Object Model)是一个应用编程接口(API),用于再HTML中使用扩展的XML。DOM将整个页面抽象为一组分层节点。

DOM通过创建表示文档的树,让开发者可以随心所欲地控制网页地内容和结构。使用DOM API,可以轻松地删除、添加、替换、修改节点。

BOM

浏览器对象模型(BOM)API,用于支持访问和操作浏览器地窗口。使用BOM,开发者可以操控浏览器显示页面之外的部分。

BOM主要针对浏览器窗口和子窗口(frame),通常会把任何特定于浏览器地扩展都归再BOM的范畴内。如下扩展:

  • 弹出新浏览器窗口的能力;
  • 移动、缩放和关闭浏览器窗口的能力;
  • navigator对象,提供关于浏览器的详尽信息;
  • location对象,提供浏览器加载页面的详尽信息;
  • screen对象,提供关于用户屏幕分辨率的详尽信息;
  • performance对象,提供浏览器内存占用、导航行为和时间统计的详尽信息;
  • 对cookie的支持
  • 其他自定义对象,如XMLHttpRequest和IE的ActiveXObject。

小结

JavaScript是一门用来与网页交互的脚本语言,包含一下三个组成部分。

  • ECMAScript:由ECMA-262定义并提供核心功能。
  • 文档对象模型(DOM):提供与网页内容交互的方法和接口。
  • 浏览器对象模型(BOM);提供与浏览器交互的方法和接口。

​ JavaScript 的这三个部分得到了五大 Web 浏览器(IE、Firefox、Chrome、Safari 和 Opera)不同程度
的支持。所有浏览器基本上对 ES5(ECMAScript 5)提供了完善的支持,而对 ES6(ECMAScript 6)和
ES7(ECMAScript 7)的支持度也在不断提升。这些浏览器对 DOM 的支持各不相同,但对 Level 3 的支
持日益趋于规范。HTML5 中收录的 BOM 会因浏览器而异,不过开发者仍然可以假定存在很大一部分
公共特性。

(36-第二章20220217+a0816

**

“script”标签元素有八个属性:

  • async:表示立即开始下载脚本,但是但是不会阻止其他页面动作,只对外部脚本有效。[可省略]

  • charset:规定代码的字符集,但是几乎没有浏览器在乎它的值。[可省略]

  • crossorigin:配置相关请求的CORS(跨资源共享)设置,有两个值:

    • 当设置为crossorigin=“anonymous”时,配置文件请求不必设置凭据标志。
    • 当设置为crossorigin=“use-credentials”时,设置凭据标志,意味着出战请求会包含凭据。[可省略]
  • defer:等页面文档被解析完了并且显示出来之后,再解析脚本,只对外部脚本文件有效。(IE7以前,对网页中的代码也可以生效)[可省略]

  • integrity:用于比对签名的一个属性,比对失败则脚本不执行。[可省略]

    原话:允许比对接收到的资源和指定的的加密签名以验证子资源完整性(SRI,Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错,脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容。

  • language:浏览器都不用了,我懒得写了。

  • src:引入外部文件[可省略]

  • type:表示代码块中脚本语言的内容类型(也叫它MIME类型)。当出现type=“module“,就会把当前代码当为ES6模块,这个时候代码中才能使用import和export关键字![可省略]

    (其实原文还提了四个可选值,但是前两个【text/javascript】和【text/ecmascript】被废弃掉了。还有后两个【application/javascript】和【application/ecmascript】可能会导致代码被忽略。记不记的吧。

”script“标签就俩用:要么在网页里写代码;要么就引入外部js文件。

出现了第一段代码->是在网页(.html文件)里面写个js函数;

 <script>
        function SayHi(){
            console.log("Hi");
            console.log("<\/script>");//这里不能直接写script结束标签,需要在斜杠之前写个转义字符
        }
        SayHi()
    </script>

不能直接写script结束标签,会报错,解析器会提前结束script代码,需要加个\转义字符。

第二段代码->在网页中引入外部js文件。

   <script src="./example.js"></script>

解释外部文件也会消耗一定的时间。

原文用的阻塞时间:阻塞时间包括下载外部js的时间+解释外部js文件代码的时间;

在XHTML文件里可以不用写结束标签。

注意:按照惯例,外部 JavaScript 文件的扩展名是.js。这不是必需的,因为浏览器不会检查所包含 JavaScript 文件的扩展名。这就为使用服务器端脚本语言动态生成 JavaScript 代码,或者在浏览器中将 JavaScript扩展语言(如TypeScript,或React的 JSX)转译为JavaScript
提供了可能性。不过要注意,服务器经常会根据文件扩展来确定响应的正确 MIME 类型。如果不打算使用.js 扩展名,一定要确保服务器能返回正确的 MIME 类型。

懒,自己看注意事项。

值得一提的是:如果script标签如果用了src属性,则标签内代码会被忽略;

script的src可以引用外部域的JS文件,所以src文件内容可以是以一个完整URL。URL指向的资源可以和HTML页面不在同一个域中如:

    <script src="http://www.somewhere.com/afile.js"></script> 

解析过程:

  • 首先会想url发送get请求
  • 如果是js文件,则刚开始的请求不受浏览器同源策略的限制
  • 但是返回被执行的JS会受限制。
  • 该请求依旧会受父页面HTTP/HTTPS协议的限制。

引入外部域代码会被当成自己页面的一部分来加载和解释;所以就可以引入不同域分发的JS。但是注意外域js文件是别人的,所以别人可以替换掉文件。所以前面的integrity属性就有用了,虽然这个属性不是所有浏览器支持。

如果script标签没有defer和async属性。则会逐行解释。

标签位置

就是讲了个:把script标签放body标签结束位置就行了。

<!DOCTYPE html>
<html>
 <head>
 <title>Example HTML Page</title>
 </head>
 <body>
 <!-- 这里是页面内容 -->
 <script src="example1.js"></script>
 <script src="example2.js"></script>
 </body>
</html> 

推迟执行脚本

当script标签定义了defer属性。这时候:

  • 浏览器会先下载文件
  • 然后接着解析并加载页面
  • 当页面被加载和解析完了,再回来执行js代码。

设置了defer虽然会在页面加载后再读取js文件代码,但他们也会在事件DOMContentLoaded事件之前执行,只适用于外部脚本。

异步脚本

当script标签定义了async属性,async与defer类似:

  • 都会立即开始下载

但是不同的是ansync的脚本并不能保证按次序执行。异步脚本保证会在页面的load事件前执行。其他不保证……

动态加载脚本

通过DOM API,向DOM中动态添加script 然后通过dom操作实现加载脚本:

  • 首先创建script元素,然后将它添加到DOM里
  <script>
        let script = document.createElement('script');
         script.src = './example.js';
        document.head.appendChild(script); 
    </script>

默认情况下,以上代码创建的script标签是以异步方式加载的,就像加了个async属性。所以可以这样写:

let script = document.createElement('script');
script.src = 'gibberish.js';
script.async = false;
document.head.appendChild(script); 

这样可以统一动态脚本的加载行为。

以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先
级。根据应用程序的工作方式以及怎么使用,这种方式可能会严重影响性能。要想让预加载器知道这些
动态请求文件的存在,可以在文档头部显式声明它们:

 <link rel="preload" href="gibberish.js"> 

XHTML中的变化

可扩展超文本标记语言(XHTML,Extensible HyperText Markup Language)是将 HTML 作为 XML的应用重新包装的结果。

与 HTML 不同,在 XHTML 中使用 JavaScript 必须指定 type 属性且值为text/javascript,HTML 中则可以没有这个属性。

(因为不是重点,略。其实主要因为我懒,不想总结。)

废弃的语法

我也将它废弃了,又不用了。学它干嘛?

行内代码与外部文件

先讲了使用外部js文件的好处:

  • 可维护性 :一个窗口专门写HTML另一个窗口专门写JS不香嘛?而且写完JS还能应用到多个HTML文件里。
  • 缓存:如果俩页面引用一个js文件,浏览器就利用缓存少加载一次文件,这不更快了?起飞!
  • 适应未来:也没啥,我这个小白看了纯属扯淡。

具体讲了个SPDY/HTTP2的好处与实现,首先了解一下SPDY:

SPDY(读作“SPeeDY”)是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。SPDY并不是一种用于替代HTTP的协议,而是对HTTP协议的增强。新协议的功能包括数据流的多路复用、请求优先级以及HTTP报头压缩。谷歌表示,引入SPDY协议后,在实验室测试中页面加载速度比原先快64%。

然后书讲到SPDY这个东西,会将可能会加载到的js文件都放入到浏览器的缓存内。这样会提高加载速度。

文档模式

IE5.5产生了文档模式的概念,使用doctype可以切换文档模式;

开始文档模式分两种:

  • 混杂模式(quirks mode)

    混杂模式可以让IE像IE5一样不标准。
    
  • 标准模式(standards mode)

    标准模式可以让IE有兼容标准的行为
    

但是主要区别在于CSS渲染方面,JS只是有一些关联的副作用。

开启混杂模式:不写<DOCTYPE>就开了。

开启标准模式:

  • 在HTML文件开头加下列代码开启HTML4.0标准模式:

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
    "http://www.w3.org/TR/html4/strict.dtd">
    
  • 在XHTML文件开头加下列代码开启XHTML标准模式:

    <!DOCTYPE html PUBLIC
    "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
    
  • 在HTML文件开头加下列代码开启HTML5标准模式:

    <!DOCTYPE html> 
    

还有个准标准模式,因为没看见HTML5的代码。懒得搬运了。可以当作只有混杂模式标准模式两种。

元素

首先它解释的是:这个标签被用于不支持JavaScript的浏览器提供代替内容。

然后现在的情况是:所有浏览器都100%支持JavaScript。

懂吧……

测试代码(可以在chrome浏览器关闭javascript脚本实现)

<!DOCTYPE html>
<html>
 <head>
 <title>Example HTML Page</title>
 </head>
 <body>
 <noscript>
 <p>This page requires a JavaScript-enabled browser.</p>
 </noscript>
 </body>
</html> 

好了这一小章节纯粹浪费时间……靠呗啦!

小结

JavaScript要么用

本章总结如下(开始搬运)

  • 要包含外部 JavaScript 文件,必须将 src 属性设置为要含文件的 URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。
  • 所有

(46-第三章20220218+a1043

语法

区分大小写

ECMAScript一切都区分大小写。

标识符

标识符就是变量、函数、属性或函数参数的名称。

标识符由一个或多个下列字符组成:

  • 第一个字符必须是一个字母、下划线( _ )或美元符号( $ );
  • 剩下的其他字符可以是字母、下划线、美元符号或数字。

推荐:书写标识符的时候,一般建议使用驼峰大小写形式,即第一个单词首字母小写,后面每个单词的首字母大写,如

firstSecond
myCar
doSomethingImportant

注意

关键字、保留字、布尔值和null不能作为标识符。

注释

单行注释://

多行注释:/* */

严格模式

ES5增加了严格模式(strict mode)的概念。

开启严格模式后,一些不规范的代码写法会报错。

给整个脚本开启严格模式:

在脚本第一行加一句:

"use strict";

也可以给单独函数体开启严格模式:

在函数开头加一句:

function doSometing(){
  "use strict";
  //函数体
}

语句

语句后面的分号( ; )建议加上。

不加也没事,解析器会补上(稍微耗费性能)

(加分号减少报错的可能性)

还有尽量使用代码块,可以放内容更清晰减少报错的可能性。

if (test) {
 test = false;
 console.log(test);
} 
// 有效,但容易导致错误,应该避免
if (test)
 console.log(test);
// 推荐
if (test) {
 console.log(test);
} 

关键字与保留字

关键字不能作为标识符或属性名;

ES6以后关键字有:

break do in typeof
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this default

if throw delete import try

ES6的保留字:

始终保留:
enum
严格模式下保留:
implements package public
interface protected static
let private
模块代码中保留:
await

起标识符属性名避开就得了。

变量

var关键字

如果只定义变量,不赋值,则定义的变量为undefined。

变量定义后可以被重写:

var message="hi";
message=123;//可以,但是不建议。
var在函数内时的块级作用域

在函数内使用var声明变量,函数外无法访问

function test() {
	 var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!

但是有个不值得推荐的解决办法:

在函数内直接写变量名并赋值,这样写变量就变为全局变量,外部可以访问到

function test() {
 message = "hi"; // 全局变量
}
test();
console.log(message); // "hi" 
注意 虽然可以通过省略 var 操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略 var 是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 eferenceError

声明多个变量可以用逗号分隔:

var message = "hi",
 found = false,
 age = 29; 

注意变量名不要定义为eval和arguments。

var声明提升

使用var声明变量会自动提升到作用域顶部(仅仅只是变量名提升到顶部,但是并没有赋值)

function foo() {
 console.log(age);
 var age = 26;
}
foo(); // undefined 

上面代码等价于:

function foo() {
 var age;
 console.log(age);
 age = 26;
}
foo(); // undefined 

此外多次使用var声明同一变量也可以,相当于重写变量:

function foo() {
 var age = 16;
 var age = 26;
 var age = 36;
 console.log(age);
}
foo(); // 36 

let声明

let与var一样也是用于声明变量,但是let存在块级作用域,var没有块级作用域,但是var有函数作用域,比如:

if (true) {
 var name = 'Matt';
 console.log(name); // Matt
}
console.log(name); // Matt 

if (true) {
 let age = 26;
 console.log(age); // 26
}
console.log(age); 
// ReferenceError: age 没有定义

因为块级作用域是函数作用域子集,所以let也有函数作用域。

let不能重复声明:

var name;
var name;

let age;
let age; // SyntaxError;标识符 age 已经声明过了

js引擎会记录变量声明的标识符及其所在的作用域,所以可以嵌套使用相同标识符。

var name = 'Nicholas';
console.log(name); // 'Nicholas'
if (true) {
 var name = 'Matt';
 console.log(name); // 'Matt'
}

let age = 30;
console.log(age); // 30
if (true) {
 let age = 26;
 console.log(age); // 26
} 

对声明冗余报错不会因混用 let 和 var 而受影响。这两个关键字声明的并不是不同类型的变量,它们只是指出变量在相关作用域如何存在

var name;
let name; // SyntaxError
let age;
var age; // SyntaxError 
暂时性死区

let与var不一样,let声明变量之前引用变量会报错。(ReferenceError)

全局声明

let声明的变量不会称为window对象的属性(var声明的变量则会)

var name = 'Matt';
console.log(window.name); 
// 'Matt'

let age = 26;
console.log(window.age);
// undefined 

let的变量不是window对象的属性,但是let声明仍然存在于全局作用域中,对应的变量会在生命周期内存续。

条件声明

就是同一页面两段script标签。只要是在同一页面内,就不能用let声明第二次。会报错。但是var可以重复声明。

    <script>
        let name = 'Nicholas';
        let age = 36;
       </script>
       <script>
        // 假设脚本不确定页面中是否已经声明了同名变量
        // 那它可以假设还没有声明过
        if (typeof name === 'undefined') {
        let name;
        }
        // name 被限制在 if {} 块的作用域内
        // 因此这个赋值形同全局赋值
        name = 'Matt';
        try {
        console.log(age); // 如果 age 没有声明过,则会报错
        }
        catch(error) {
        let age; 
    }
 // age 被限制在 catch {}块的作用域内
 // 因此这个赋值形同全局赋值
 		age = 26;
</script> 

注意 不能使用 let 进行条件式声明是件好事,因为条件声明是一种反模式,它让程序变得更难理解。如果你发现自己在使用这个模式,那一定有更好的替代方式。

for循环中的let声明

如果for里面是个异步的逻辑,如定时器等,因为var没有块级作用域。所以,每次异步任务最终接收到的值都是for循环中计数器的最大值。

使用let不会,因为let有块级作用域,这样保持了每次接收的值都不一样。

for (let i = 0; i < 5; ++i) {
 setTimeout(() => console.log(i), 0)
}
// 会输出 0、1、2、3、4 

const声明

const用法与let一致。

不过const声明常量,声明过后不可修改。

const可以和for-of/for-in循环一起使用。

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 

声明风格及最佳实践

不使用var;

let与const都能使用的时候,选const。

数据类型

ECMAScript的6种简单数据类型(原始数据类型):

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Symbol(ES6新增)
  • 还有复杂数据类型Object

typeof 操作符

用来检测数据类型。

"undefined"表示值未定义; "boolean"表示值为布尔值; "string"表示值为字符串; "number"表示值为数值; "object"表示值为对象(而不是函数)或 null; 
"function"表示值为函数; "symbol"表示值为符号。
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number" 

(补个:typeof 的返回值是以字符串来显示返回的数值类型。如:

console.log(typeof true)//这里以布尔值为例其他数值同理
//上面返回“Boolean”
console.log(typeof typeof true)//返回String

Undefined类型

光声明不赋值,最后变量为undefined;

变量值为undefined只是声明不赋值。如果直接不声明就输出,会报错;

注意 即使未初始化的变量会被自动赋予 undefined 值,但我们仍然建议在声明变量的
同时进行初始化。这样,当 typeof 返回"undefined"时,你就会知道那是因为给定的变
量尚未声明,而不是声明了但未初始化。

测试代码:说明了声明了不赋值与不声明的区别。

let message; // 这个变量被声明了,只是值为 undefined
// age 没有声明
if (message) {
 // 这个块不会执行
}
if (!message) {
 // 这个块会执行
}
if (age) {
 // 这里会报错
} 

Null类型

null表示一个空对象指针。

只要变量要保存对象,但是还没有对象保存,就可以给变量加个null值表示该变量将来用来存储对象。

测试代码:说明了null与undefined的区别:

let message = null;
let age;
if (message) {
 // 这个块不会执行
}
if (!message) {
 // 这个块会执行
} 

if (age) {
 // 这个块不会执行
}
if (!age) {
 // 这个块会执行
} 

Boolean类型

有两个值:true和false;

注意区分大小写。

可以调用特定的Boolean()转型函数,将其他值转为布尔值;

其他数据类型转布尔值的转换规则:

数据类型转为true的值转换为false的值
Booleantruefalse
String非空字符串“”(空字符串)
Number非零数值(包括无穷值)0、NaN
Object任意对象null
UndefinedN/A(不存在)undefined

Number类型

十进制:

整数直接写出来

55

八进制:

八进制第一个数字必须是零(0),然后是响应的八进制数字(0~7)

070//有效八进制

079//无效

(八进制在严格模式下无效,会抛出错误)

十六进制:

前面必须写前缀 0x(区分大小写),然后是十六进制数字(09以及AF)。十六进制数字种的字母大小写均可。

0xA//十六进制10

0x1f//十六进制31

注意 由于 JavaScript 保存数值的方式,实际中可能存在正零(+0)和负零(0)。正零和负零在所有情况下都被认为是等同的,这里特地说明一下。

浮点值

存储浮点值使用的内存是存储整数值的两倍。所以ECMAScript会将数值尽可能转换为整数

注意:浮点值相加有bug,在js里0.1+0.2≠0.3

注意 之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript所独有。其他使用相同格式的语言也有这个问题。

检测值在无限内

判断值是否是有限大的可以使用isFinite()函数

let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result));
// false 
NaN

NaN:特殊值。意思为“不是数值”(Not a Number);

用于本来要返回的数值操作失败了(而不是抛出错误)

NaN不等于任何值,包括自己在内

判断一个值是否为NaN可以使用isNaN()函数来判断。

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 

注意 虽然不常见,但 isNaN()可以用于测试对象。此时,首先会调用对象的 valueOf()
方法,然后再确定返回的值是否可以转换为数值。如果不能,再调用 toString()方法,
并测试其返回值。这通常是 ECMAScript 内置函数和操作符的工作方式,本章后面会讨论

数值转换规则

有3个函数可以将非数值转换为数值:

Number()、parseInt()和parseFloat();

Number()可以适用于任何数据类型;

parseInt()和parseFloat()主要用于将字符串转换为数值;

​ Number()函数转换规则:

  • 布尔值:true转换为1,false转换为0
  • 数值:直接返回
  • null:返回0
  • undefined:返回NaN
  • 字符串:应用如下规则:
    • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。
    • 如果字符串包含有效的浮点值如“1.1”则会转换为相应的浮点值
    • 如果字符串包含有效的十六进制格式如“0xf”则会将其转为相应的十进制整数
    • 如果是空字符串则返回0
    • 如果字符串包含除上述情况之外的其他字符,返回NaN.
  • 对象:调用ValueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。

parseInt()函数拿到数值之后,会在第一个非空字符串开始,如果碰到的第一个数值不为数值或加减号,则会立即返回NaN。(空字符串也会返回NaN);

如果parseInt碰到的第一个字符串为数值字符、加减号,则会继续一次检测每个字符直到字符串末尾,或碰到非数值字符后。直接返回检测到的数值。

parseInt()函数的第二个参数可以指定进制数(二进制十进制等……);

parseFloat()与parseInt()类似,它可以解析到第一个小数点后面第二个小数点之前;

String类型

字符串类型,可以使用双引号(“)、单引号(‘)或反引号(`)标识。

字符字面量
字面量含义
\n换行
\t制表
\b退格
\r回车
\f换页
\\反斜杠(\)
\’单引号(‘)
……

字符串可以通过length属性获取长度:

let text = "This is the letter sigma: \u03a3."; 
console.log(text.length); // 28 
字符串特点

字符串是不可变的,每次修改某变量字符串的值,都是先销毁原字符串然后将新字符串存入变量。

转换为字符串

几乎所有值都有toString()方法。用这个方法转换字符串。

null和undefined值没有toString()方法。

如果不确定一个值是否为null或undefined。可以使用String()转型函数:

  • 如果值有toString()方法,则调用该方法(不传参数)并返回结果。
  • 如果值是null,返回”null“
  • 如果值是undefined,返回”undefined“

注意 用加号操作符给一个值加上一个空字符串""也可以将其转换为字符串

模板字面量

ES6以后增加的就是(`)其中里面换行会进行保留。

字符串插值
let value = 5;
let exponent = 'second';
// 以前,字符串插值是这样实现的:
let interpolatedString =
 value + ' to the ' + exponent + ' power is ' + (value * value);
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral =
 `${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25 

所有插入的值都会使用 toString()强制转型为字符串,而且任何 JavaScript 表达式都可以用于插
值。嵌套的模板字符串无须转义:

console.log(`Hello, ${ `World` }!`); // Hello, World!
将表达式转换为字符串时会调用 toString():
let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!
在插值表达式中可以调用函数和方法:
function capitalize(word) {
 return `${ word[0].toUpperCase() }${ word.slice(1) }`;
}
console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!
此外,模板也可以插入自己之前的值:
let value = '';
function append() {
 value = `${value}abc`
 console.log(value);
}
append(); // abc
append(); // abcabc
append(); // abcabcabc 
模板字面量标签函数

标签函数就是可以自定义插值的函数。标签函数会接收被插值记号分隔后的模板和对每个表达式求职的结果。

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 }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar" 

因为表达式参数的数量是可变的,所以通常会使用剩余操作符(rest operator)将收集到的实参存入数组中去:

let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) {
 console.log(strings);
 for(const expression of expressions) {
 console.log(expression);
 }
 return 'foobar';
}
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(taggedResult); // "foobar" 

如果想让字符串与表达式拼接:

let a = 6;
let b = 9;
function zipTag(strings, ...expressions) {
 return strings[0] +
 expressions.map((e, i) => `${e}${strings[i + 1]}`)
 .join('');
}
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" 
原始字符串
// Unicode 示例
// \u00A9 是版权符号
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
// 换行符示例
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"
// 对实际的换行符来说是不行的
// 它们不会被转换成转义序列的形式
console.log(`first line second line`);
// first line
// second line
console.log(String.raw`first line
second line`);
// first line
// second line 

另外,也可以通过标签函数的第一个参数,即字符串数组的.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`;
// Actual characters:
// ©
//(换行符)
// Escaped characters:
// \u00A9
// \n 

Symbol类型

Symbol(符号)是ES6新增数据类型。

Symbol实例是唯一、不可变的。

Symbol的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。(确保对象属性是独一无二的)

Symbol就是用来创建唯一记号,进而用作非字符串形式的对象属性。

基本用法

符号需要使用Symbol()函数初始化。因为符号本身是原始数据类型。所以typeof 操作符对符号返回的是symbol:

let sym = Symbol();
console.log(typeof sym); // symbol 

Symbol()函数可以传入字符串参数作为对Symbol的描述(description),具体作用不清楚:

let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false 
console.log(fooSymbol == otherFooSymbol); // false 

只要创建Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性

let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()
let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo); 

最重要的是,Symbol()函数不能与new关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象:

let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"
let myString = new String();
console.log(typeof myString); // "object"
let myNumber = new Number();
console.log(typeof myNumber); // "object"
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor 

使用符号包装对象,可以借用Object()函数:

let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); 
// "object" 
使用全局符号注册表

Symbol.for()方法对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在于该字符串对应的符号,然后就会返回该符号实例。

let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true 

即使采用相同的符号描述,在全局注册表中定义符号跟使用Symbol()定义的符号也并不相同:

let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false 

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。

let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined) 

Symbol.keyFor()函数用来查询全局注册表,这个方法接收符号,返回全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined。

串键。如果查询的不是全局符号,则返回 undefined。
// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError:
Symbol.keyFor(123); // TypeError: 123 is not a symbol 
使用Symbol符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.defineProperties()定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。

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)] 

因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果
没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:

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/));
console.log(barSymbol);
// Symbol(bar) 
常用内置符号

这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。

注意 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,
@@iterator 指的就是 Symbol.iterator。
Symbol.asyncIterator

这个符号作为一个属性表示“一个方法,该方法返回对象默认的 AsyncIterator。由 for-await-of 语句使用”。

for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以 Symbol.asyncIterator
为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API
的 AsyncGenerator:

class Foo {
 async *[Symbol.asyncIterator]() {}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}

技术上,这个由 Symbol.asyncIterator 函数生成的对象应该通过其 next()方法陆续返回Promise 实例。可以通过显式地调用 next()方法返回,也可以隐式地通过异步生成器函数返回:

class Emitter {
 constructor(max) {
 this.max = max;
 this.asyncIdx = 0;
 } 
 async *[Symbol.asyncIterator]() {
 while(this.asyncIdx < this.max) {
 yield new Promise((resolve) => resolve(this.asyncIdx++));
 }
 }
}
async function asyncCount() {
 let emitter = new Emitter(5);
 for await(const x of emitter) {
 console.log(x);
 }
}
asyncCount();
// 0
// 1
// 2
// 3
// 4 
注意 Symbol.asyncIterator 是 ES2018 规范定义的,因此只有版本非常新的浏览器
支持它。关于异步迭代和 for-await-of 循环的细节,参见附录 A。
Symbol.hasInstance

这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用”。instanceof 操作符可以用来确定一个对象实例的原型链上是否有原型。instanceof 的典型使用场景如下:

function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true 

在 ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。以 Symbol.hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下:

function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true 
Symbol.isConcatSpreadable

这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat()打平其数组元素”。ES6 中的 Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖 Symbol.isConcatSpreadable
的值可以修改这个行为。

数组对象默认情况下会被打平到已有的数组,false 或假值会导致整个对象被追加到数组末尾。类
数组对象默认情况下会被追加到数组末尾,true 或真值会导致这个类数组对象被打平到数组实例。其
他不是类数组对象的对象在 Symbol.isConcatSpreadable 被设置为 true 的情况下将被忽略。

let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(array)); // ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array(1)]
let arrayLikeObject = { length: 1, 0: 'baz' };
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']
let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)]
otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo'] 
Symbol.iterator

这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用”。

Symbol.match

这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”。String.prototype.match()方法会使用以 Symbol.match 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数:

console.log(RegExp.prototype[Symbol.match]);
// ƒ [Symbol.match]() { [native code] }
console.log('foobar'.match(/bar/));
// ["bar", index: 3, input: "foobar", groups: undefined] 

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match 函数接收一个参数,就是调用 match()方法的字符串实例。返回的值没有限制:

class FooMatcher {
 static [Symbol.match](target) {
 return target.includes('foo');
 }
}
console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false
class StringMatcher {
 constructor(str) {
 this.str = str;
 }
 [Symbol.match](target) {
 return target.includes(this.str);
 }
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false 
Symbol.replace

这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”。String.prototype.replace()方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数:

console.log(RegExp.prototype[Symbol.replace]);
// ƒ [Symbol.replace]() { [native code] }
console.log('foobarbaz'.replace(/bar/, 'qux'));
// 'fooquxbaz' 

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法
直接使用参数,可以重新定义 Symbol.replace 函数以取代默认对正则表达式求值的行为,从而让
replace()方法使用非正则表达式实例。Symbol.replace 函数接收两个参数,即调用 replace()方
法的字符串实例和替换字符串。返回的值没有限制:

class FooReplacer {
 static [Symbol.replace](target, replacement) {
 return target.split('foo').join(replacement);
 }
}
console.log('barfoobaz'.replace(FooReplacer, 'qux'));
// "barquxbaz"
class StringReplacer {
 constructor(str) {
 this.str = str;
 }
 [Symbol.replace](target, replacement) {
 return target.split(this.str).join(replacement);
 }
}
console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux'));
// "barquxbaz
Symbol.search

“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search()方法使用”

Symbol.species

一个函数值,该函数作为创建派生对象的构造函数

Symbol.split

一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split()方法使用

Symbol.toPrimitive

“一个方法,该方法将对象转换为相应的原始值。由 ToPrimitive 抽象操作使用”

Symbol.toStringTag

“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用

Symbol.unscopables

一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除

不推荐使用 with,因此也不推荐使用 Symbol.unscopables。

(注意!因为Symbol方法众多,精细阅读的话,就违背了我懒的初衷,所以我删减细节。主要因为我技术菜,我也总结不了Symbol)

Object类型

对象就是一组数据和功能的集合。

使用new 操作符创建对象:

let o=new Object()

每个Object都有如下的属性和方法:

  • constructor:用于创建当前对象的函数。
  • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查属性名必须是字符串(如o.hasOwnProperty(“name”))或符号。
  • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。
  • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in语句枚举。与hasOwnProperty()一样,属性名必须是字符串。
  • toLocaleString():返回对象的字符串表示,该字符串反应对象所在的本地化执行环境。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。

因为ECMAScript中Object是所有对象的基类,所以任何对象都有这些属性和方法。

注意 严格来讲,ECMA-262 中对象的行为不一定适合 JavaScript 中的其他对象。比如浏览器环境中的 BOM 和 DOM 对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受 ECMA-262 约束,所以它们可能会也可能不会继承 Object。

(81-操作符20220220+b0209

一元操作符

递增/递减操作符

前缀递增/递减会给数值加1/减1:

let	age=10;
++age;
let age=29;
--age;

前缀递增/减都会在语句被求值之前改变。

前缀递增/减在语句中优先级是相等的,所以会从左到右依次求值。

后缀版的递增和递减在语句被求值后才发生。

let age=10;
age++;
let age=29;
age--;

这 4 个操作符可以作用于任何值,意思是不限于整数——字符串、布尔值、浮点值,甚至对象都可以。递增和递减操作符遵循如下规则:

  • 对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串编程数值。
  • 对于字符串,如果不是有效的数值形式,则将变量的值设置为NaN。变量类型从字符串变成数值。
  • 对于布尔值,如果是false,则转换为0再应用改变。变量类型从布尔值变成数值。
  • 对于布尔值,如果是true,则转换为1再应用改变。变量类型从布尔值变成数值。
  • 对于浮点值,加1或减1.
  • 如果是对象,则调用其valueOf()方法取得可以操作的值。对得到的值应用上述规则。如果是NaN,则调用toString()并再次应用其他规则。变量类型从对象变成数值。
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 
一元加和减

将加号放在变量前面就相当于执行了Number()一样,会将变量转为Number类型:

let s1 = "01";
let s2 = "1.1"; 
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
 valueOf() {
 return -1;
 }
};
s1 = +s1; // 值变成数值 1
s2 = +s2; // 值变成数值 1.1
s3 = +s3; // 值变成 NaN
b = +b; // 值变成数值 0
f = +f; // 不变,还是 1.1
o = +o; // 值变成数值-1 

将减号放在变量前面一样也会先执行数据转换,再将转换为Number的数据取反:

let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
 valueOf() {
 return -1;
 }
};
s1 = -s1; // 值变成数值-1
s2 = -s2; // 值变成数值-1.1
s3 = -s3; // 值变成 NaN
b = -b; // 值变成数值 0
f = -f; // 变成-1.1
o = -o; // 值变成数值 1 
位操作符
按位非

波浪符( ~ )

let num1 = 25; // 二进制 00000000000000000000000000011001
let num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26 
按位与

and符(&)

let num1 = 25; // 二进制 00000000000000000000000000011001
let num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26 
按位或

管道符( | )

let result = 25 | 3;
console.log(result); // 27 
按位异或

脱字符( ^ )

let result = 25 ^ 3;
console.log(result); // 26 
左移

两个小于号(<<)

let oldValue = 2; // 等于二进制 10
let newValue = oldValue << 5; // 等于二进制 1000000,即十进制 64 
有符号右移

两个大于号(>>)

let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >> 5; // 等于二进制 10,即十进制 2
无符号右移

3个大于号(>>>)

let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >>> 5; // 等于二进制 10,即十进制 2 

布尔操作符

布尔操作符一共三个:逻辑非、逻辑与、逻辑或。

逻辑非

逻辑非操作符是一叹号( ! )

它的作用就是将变量转换为布尔值后取反,有如下规则:

  • 如果操作数是对象,则返回false。

  • 如果操作数是空字符串,则返回true。

  • 如果操作数是非空字符串,则返回false。

  • 如果操作数是数值0,则返回true。

  • 如果操作数是非0数值(包括Infinity),则返回false。

  • 如果操作数是null,则返回true。

  • 如果操作数是NaN,则返回true。

  • 如果操作数是undefined,则返回true。

    console.log(!false); // true
    console.log(!"blue"); // false
    console.log(!0); // true
    console.log(!NaN); // true
    console.log(!""); // true
    console.log(!12345); // false 
    
逻辑与

逻辑与操作符由两个和符号组成(&&),逻辑与遵循如下规则:

  • 如果第一个操作数是对象,则返回第二个操作数
  • 如果人第二个操作数是对象,则只有一个操作数求值为true才会返回该对象。
  • 如果两个操作数都是对象,则返回第二个操作数。
  • 如果有一个操作数是null,则返回null。
  • 如果有一个操作数是NaN,则返回NaN。
  • 如果有一个操作数是undefined,则返回undefined。

逻辑与操作符是一种短路操作符:如果第一个是false,则不会读到后面了。如果第一个为true,则会看第二个,如果都是true,则返回第二个。

(两个条件都为真才为真,有一个为假,结果就是假)

逻辑或

逻辑或由两个管道符组成(||),逻辑或遵循如下规则:

  • 如果第一个操作数是对象,则返回第一个操作数。
  • 如果第一个操作数求值为false,则返回第二个操作数。
  • 如果两个数都是对象,则返回第一个操作数。
  • 如果两个操作数都是null,则返回null。
  • 如果两个操作数都是NaN,则返回NaN。
  • 如果两个操作数都是undefined,则返回undefined。

与逻辑的短路特性:当第一个为true,则不会读到第二个。

(有一个为真,则为真。全假才为假)

乘性操作符

乘法操作符

乘法操作符(*)

基本特性与初中学的乘法吻合:

  • 当JS无法表示乘积会返回Infinity或-Infinity。
  • 如果有任一操作数为NaN,则返回NaN。
  • 如果Infinity乘以0,则返回NaN.
  • 如果有不是数值的操作符,则会先用Number()将其转为数值,再计算。
除法操作符

除法操作符由一个斜杠表示(/),除法遵循规则:

  • 如果JS无法表示结果则返回Infinity或-Infinity。
  • 如果由任一操作符是NaN,则返回NaN.
  • 如果是Infinity除以Infinity,则返回NaN
  • 如果0除以0,返回NaN
  • 如果除以0,则会返回Infinity或-Infinity
  • Infinity除以任何数都是Infinity
  • 如果存在不是数值的,则先运行Number()再进行相除。
取模操作符

取模(余数)操作符由一个百分比符号(%)表示,取模操作符遵循规则:

  • 操作符都为数值,进行除法运算返回余数。
  • 被除数为无限值(Infinity),则返回NaN
  • 如果被除数是有限值,除数是无限值,则返回被除数。
  • 如果被除数是0,除数不是0返回0
  • 不是数值的先Number()转换再执行运算。
指数操作符

指数操作符=》Math.pow()与指数操作符一样(* *)

console.log(Math.pow(3, 2); // 9
console.log(3 ** 2); // 9
console.log(Math.pow(16, 0.5); // 4
console.log(16** 0.5); // 4 

也可以这样写:

let sqrt = 16;
sqrt **= 0.5;
console.log(sqrt); // 4 

加性操作符

加法操作符

加法操作符(+)用于求两个数的和

如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:

  • 如果有任一操作数是 NaN,则返回 NaN;
  • 如果是 Infinity 加 Infinity,则返回 Infinity;
  • 如果是-Infinity 加-Infinity,则返回-Infinity;
  • 如果是 Infinity 加-Infinity,则返回 NaN;
  • 如果是+0 加+0,则返回+0;
  • 如果是-0 加+0,则返回+0;
  • 如果是-0 加-0,则返回-0。

不过,如果有一个操作数是字符串,则要应用如下规则:

  • 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。
let result1 = 5 + 5; // 两个数值
console.log(result1); // 10
let result2 = 5 + "5"; // 一个数值和一个字符串
console.log(result2); // "55" 
减法操作符

减法操作符(-)也是使用很频繁的一种操作符,与加法操作符一样,减法操作符也有一组规则用于处理 ECMAScript 中不同类型之间的转换:

  • 如果两个操作数都是数值,则执行数学减法运算并返回结果。
  • 如果有任一操作数是 NaN,则返回 NaN。
  • 如果是 Infinity 减 Infinity,则返回 NaN。
  • 如果是-Infinity 减-Infinity,则返回 NaN。
  • 如果是 Infinity 减-Infinity,则返回 Infinity。
  • 如果是-Infinity 减 Infinity,则返回-Infinity。
  • 如果是+0 减+0,则返回+0。
  • 如果是+0 减-0,则返回-0。
  • 如果是-0 减-0,则返回+0。
  • 如果有任一操作数是字符串、布尔值、null 或 undefined,则先在后台使用 Number()将其转换为数值,然后再根据前面的规则执行数学运算。如果转换结果是 NaN,则减法计算的结果是NaN。
  • 如果有任一操作数是对象,则调用其 valueOf()方法取得表示它的数值。如果该值是 NaN,则减法计算的结果是 NaN。如果对象没有 valueOf()方法,则调用其 toString()方法,然后再将得到的字符串转换为数值。
let result1 = 5 - true; // true 被转换为 1,所以结果是 4
let result2 = NaN - 1; // NaN
let result3 = 5 - 3; // 2
let result4 = 5 - ""; // ""被转换为 0,所以结果是 5
let result5 = 5 - "2"; // "2"被转换为 2,所以结果是 3
let result6 = 5 - null; // null 被转换为 0,所以结果是 5 

关系操作符

用法与数学课上的一样。结果返回布尔值 ,ECMAScript 中的其他操作符一样,在将它们应用到不同数据类型时也会发生类型转换和其他行为:

  • 如果操作数都是数值,则执行数值比较。
  • 如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
  • 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
  • 如果有任一操作数是对象,则调用其 valueOf()方法,取得结果后再根据前面的规则执行比较。如果没有 valueOf()操作符,则调用 toString()方法,取得结果后再根据前面的规则执行比较。
  • 如果有任一操作数是布尔值,则将其转换为数值再执行比较

比较字符串的时候,会比较字符串中对应字符的编码;

比较两个字符串形式的数字的时候也是比较它们的字符编码去比较。

如果有一个为数值,则会按照数值比较。

(95-相等操作符20220221+b0112

等于和不等于

ECMAScript中等于(==)和不等于(!=)两个操作符在进行操作的时候会进行强制类型转换:

在转换操作数的类型时,相等和不相等操作符遵循如下规则。

  • 如果操作数有布尔值(Boolean)则会转换为数值(Number)进行比较。false 转换为 0,true 转换
    为 1。
  • 字符串与数字比较时,会尝试将字符串转为数字然后进行比较。
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再
    根据前面的规则进行比较。

在进行比较时,这两个操作符会遵循如下规则。

  • null 和 undefined 相等。
  • null 和 undefined 不能转换为其他类型的值再进行比较。
  • 如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两
    个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,
    则相等操作符返回 true。否则,两者不相等。
全等和不全等

全等(=)和不全等(!)false 转换为 0,true 转换为 1。

注意 由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。
这样有助于在代码中保持数据类型的完整性。

条件操作符

variable = boolean_expression ? true_value : false_value;

上面的代码执行了条件赋值操作,即根据条件表达式 boolean_expression 的值决定将哪个值赋给变 量 variable 。 如果 boolean_expression 是 true , 则 赋 值 true_value ; 如 果boolean_expression 是 false,则赋值 false_value。

(前面表达式如果为真返回第一个,如果为假,返回第二个)

赋值操作符

简单赋值用等于号(=)表示,将右手边的值赋给左手边的变量;

复合赋值使用乘性、加性或位操作符后跟等于号(=)表示。

  • 乘后赋值(*=)
  • 除后赋值(/=)
  • 取模后赋值(%=)
  • 加后赋值(+=)
  • 减后赋值(-=)
  • 左移后赋值(<<=)
  • 右移后赋值(>>=)
  • 无符号右移后赋值(>>>=)

这些操作符仅仅是简写语法,使用它们不会提升性能。

逗号操作符

逗号操作符可以用来在一条语句中执行多个操作;

可以使用逗号操作符来辅助赋值。在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:

let num = (5, 1, 4, 8, 0); // num 的值为 0

语句

if语句

if(表达式1){
代码块1
}else if(表达式2){
代码块2
}else{
代码块3
}

(当读到if的时候:

  • 首先去读取"表达式1":
    • 如果为true则会进入到"代码块1",然后执行"代码块1"里的逻辑,随后忽略if的其他代码块。
    • 如果为false则会读取"表达式2":
      • 如果为true则会进入到"代码块2",然后忽略剩余的代码块。
      • 如果为false,则会进入到"代码块3"

do-while语句

do {
代码块
} while (表达式);

下面是一个例子:
let i = 0;
do {
i += 2;
} while (i < 10);

do-while语句会先执行一次前面的代码块,然后再进行表达式判断。

while语句

while(表达式) {
代码块}

while语句会先执行表达式,如果表达式成立,则会进入代码块执行。

for语句

for (initialization; expression; post-loop-expression){statement} 
for (let i = 0; i < count; i++) {
console.log(i);
}
for(表达式1:定义计数器;表达式2:为循环设置限制;表达式3:让计数器发生改变){

执行体}

for循环首先会执行表达式1然后再接着执行表达式2:

  • 如果表达式2满足条件,则会进入执行体,执行体执行完毕了会进入表达式3;
  • 执行完表达式3后则会继续执行表达式1,然后执行表达式2……
  • 如果不满足表达式2,则会直接跳出循环。

注意:for循环的表达式都不是必须的,所以可以省略。

for (;;) { // 无穷循环
doSomething();
}

for-in语句

for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性

for (property in expression) statement
for (const propName in window) {
document.write(propName);
}

注意:

  • for-in可以循环遍历所有对象里面的属性
  • 如果for-in循环要迭代的变量是null或undefined,则不会执行循环体。

for-of语句

for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素

(可迭代对象的元素:首先要求是可迭代,那么要遍历的对象是可迭代的:如数组、含有迭代器的对象等)

注意:如果尝试迭代的变量不支持迭代,则 for-of 语句会抛出错误。

标签语句

label: statement
例子:
start: for (let i = 0; i < count; i++) {
console.log(i);
}

标签语句的典型应用场景是嵌套循环

break和continue

使用break会立即退出循环体,然后执行循环下边的语句代码。

使用continue会立即退出当次循环,然后执行下一次循环,直到表达式不被满足退出循环体。

break和continue可以配合标签语句执行。

break:

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(`j=${j}`)
    }
    console.log(`iiiiiiiiiiii=${i}`)
}
console.log(num); // 55

continue:

let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
            continue outermost;
        }
        num++;
        console.log(`j=${j}`)
    }
    console.log(`iiiiiiiiiiii=${i}`)
}
console.log(num); // 95

break案例表示当if判断满足时,break直接退出到标签带有的循环体。

continue案例表示当if判断满足时,continue直接退出当前循环(因为标签语句所以会回退到标签语句标记的循环处)开始下一次循环。

组合使用标签语句和 break、continue 能实现复杂的逻辑,但也容易出错。注意标签要使用描述性强的文本,而嵌套也不要太深。

with语句

with 语句的用途是将代码作用域设置为特定的对象

with (expression) statement;
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;
上面代码中的每一行都用到了 location 对象。如果使用 with 语句,就可以少写一些代码:
with(location) {
let qs = search.substring(1);
let hostName = hostname;
let url = href;
}

严格模式不允许使用 with 语句,否则会抛出错误。

警告 由于 with 语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用 with
语句

switch语句

switch (expression) {
case value1:
statement
break;
case value2:
statement
break;
case value3:
statement
break;
case value4:
statement
break;
default:
statement
}
switch (i) {
case 25:
console.log("25");
break;
case 35:
console.log("35");
break;
case 45:
console.log("45");
break;
default:
console.log("Other");
}

在switch语句中,为了避免不必要的判断,最好在每条后面添加break,如果的确需要多次判断,建议加注释说明。

在js中switch表达式可以是常量也可以是变量可而且可以是任何数据类型!

注意 switch 语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类
型(比如,字符串"10"不等于数值 10)。

函数

function functionName(arg0, arg1,...,argN) {
statements
}

调用函数使用函数名后加括号形式:

function sayHi(name, message) {
console.log("Hello " + name + ", " + message);
}
sayHi("Nicholas", "how are you today?");

return语句:如果函数内代码执行到return后,则会直接跳出函数体(return后面的代码不会被执行).

函数也可以不带返回值,不带return的话,最后函数会返回undefined。

注意 最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。

严格模式对函数也有一些限制:

  • 函数不能以 eval 或 arguments 作为名称;
  • 函数的参数不能叫 eval 或 arguments;
  • 两个命名参数不能拥有同一个名称。

如果违反上述规则,则会导致语法错误,代码也不会执行。

小结

  • ECMAScript 中的基本数据类型包括 Undefined、Null、Boolean、Number、String 和 Symbol。
  • ECMAScript 不区分整数和浮点值,只有 Number 一种数值数据类型。
  • Object 是一种复杂数据类型,它是这门语言中所有对象的基类。(万物皆对象)
  • 严格模式为这门语言中某些容易出错的部分施加了限制(语言不规范就会报错)。
  • ECMAScript 提供了很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等。
  • ECMAScript 中的函数与其他语言中的函数不一样,其余分支语句都是借鉴其他语言。
  • 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
  • 不指定返回值的函数实际上会返回特殊值 undefined。

(108-第四章20220313+a1138

原始值与引用值

原始值(primitive value):最简单的数据;

引用值(reference value):由多个值构成的对象;

他们都是JS的数据

原始值在内存中可以直接取到相应的数据;

引用值在内存中储存的是一段地址,地址对应的空间里储存相应的数据.

动态属性

原始值不能有属性,添加属性不会报错但是查询不到.

let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined

使用new关键字JS会创建一个Object类型的实例,但是它和原始值类似.

let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age); // undefined
console.log(name2.age); // 26
console.log(typeof name1); // string
console.log(typeof name2); // object

复制值

复制值:

let num1;
let num2=num1//复制值

以上为书中所讲的复制值.

主要内容为:

  • 原始值:原始值变量会把自己储存的赋给新值变量:修改新值变量不会影响到旧的变量.
  • 引用值:引用值变量会把自己储存的内存地址(就是一堆十六进制的地址)赋给新值变量:修改新值变量会影响到旧的变量,因为储存的地址一样,所以两个变量都指向同一内存空间

传递参数

前提:传递参数与函数有关:函数定义形参然后外界可以通过调用并传递实参.

传递参数的过程就是将外部的变量作为实参复制给形参(形参是函数内部定义的变量)

结合上一篇**复制值**可以得出:

  • 当传入为原始值变量时:
    • 内部会获得形参的数据,并且可以操作,但是如果不return出来的作为函数的返回值的话,那么内部的值会在函数调用结束后销毁(调用结束:就是函数大括号内代码执行完毕)
  • 当传入为引用值变量时:
    • 内部会获得形参的储存地址:内部增加或者删除属性会影响到外部的引用值变量的内容,注意:当形参在函数体内部重构后(就是重写成新的原始值或者new一个新的引用值),该参数会在函数调用结束后销毁.

注意 ECMAScript 中函数的参数就是局部变量。

确定类型

这里主要讲到如何判断变量的类型(原始/引用)

typeof操作符

typeof可以很轻松判断原始值,但是无法判断引用值的类型.

instanceof操作符

通过 instanceof 操作符检测任何引用值和
Object 构造函数都会返回 true;如果用 instanceof 检测原始值,则始终会返回 false

let rex=new RegExp('[a-z]')
console.log(typeof rex)//object
console.log("-------------")
function a(){  }//创建构造函数
let b=new a()//通过new实例化对象
let c={}
console.log(b instanceof a)//true
console.log(c instanceof a)//false

执行上下文与作用域

context n. 背景,环境;上下文,语境

(我习惯叫它环境…书中叫它上下文,那就叫上下文吧.)

首先以我的思路来进行,先讲内存存储模式:

(内存存储模式有两种:堆内存栈内存

堆内存就是***引用变量***存储数据的地方,特点是这里数据可以随意堆砌.

栈内存是:***原始值变量***存储数据的地方;***引用变量***存储地址的地方(地址指向堆内存中对应的空间,空间内有相应的数据),特点是先进后出.)

而执行上下文在栈内存中,每个上下文都有一个关联的变量对象(variable object)而这个上下文中定义的所有变量和函数都存在于这个对象上。

全局上下文与js执行环境有关,比如浏览器的全局上下文就是window对象;

(前提:js执行环境为浏览器)所有通过var定义的变量和函数都会成为window对象的属性和方法.(let与const不会)

每个函数当被调用后,会新生成新的上下文放到栈里执行,当函数执行完后,会从栈内弹出,然后继续运行其他上下文.

作用域链(scope chain):(简单总结一下)作用域链是线性且有序的,内层函数可以访问上层甚至上上层的变量或者函数,但是外层的(变量或者函数)无法访问内层的变量或者函数

  • 当内层作用域内有需要拿数据的情况,首先会从自身查找,是否有相应的数据->然后会依次去上层或上上层逐层查找,直到找到最外层作用域,如期间找到:则讲数据拿到,如找不到,会去原型(是去__proto__不是去prototype)内逐层查找,如果原型内也找不到,则会返回undefined

作用域链增强

当使用:

  • tyr/catch语句的catch块
  • with语句

的时候,都会在作用域链前添加一个变量对象.

with会在括号包含的上下文作用域的外层添加一层作用域,添加之后,代码块里可以访问到被添加的那一层作用域内的变量或者函数

catch会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明.(错误也被当作变量被访问并弹出,在catch内也可以写自己的逻辑.)

变量声明

var声明会有变量提升:即var声明的东西会被拿到(函数作用域或全局作用域)的顶部,位于所有作用域之前.

  • let/const不会有变量提升,提前引用会报错
  • let/const不能重复声明
  • let/const存在块级作用域,但是var只有函数作用域.
  • let声明变量,const声明常量
  • 在平时选择的时候,如果不改动数据,选择const
  • const创建对象之后,还能修改对象键的值.
  • 如果想让对象无法修改,可以使用Object.freeze(),使用之后再为对象赋值不报错,但会静默失败(静默失败:不报错也不会生效的语句.)

注意 开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用const 声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现重新赋值导致的 bug

  • 如果局部作用域和全局作用域中存在同名变量,则局部内搜索会以局部内的同名变量为基准,拿到了局部内的变量就会停止搜索,不会再去拿外面的变量了.

垃圾回收

标记清理

就是给要回收的变量添加标记然后回收内存.

引用计数

就是监听每个变量被指向的指针,如果为0或者弱引用,则进行回收.

内存管理

  • 平时的时候可以将不用的变量指向null来解除引用.
  • 使用const和let声明变量可以提高性能,因为const/let都以块为作用域,可以更快的进行垃圾回收.
  • 删除隐藏类
  • 注意内存泄漏,闭包会导致内存泄露.
  • (我不做深入了解,了解大概即可)

小结

JavaScript 变量可以保存两种类型的值:原始值和引用值。原始值可能是以下 6 种原始数据类型之
一:Undefined、Null、Boolean、Number、String 和 Symbol。原始值和引用值有以下特点。

  • 原始值大小固定,因此保存在栈内存上。
  • 从一个变量到另一个变量复制原始值会创建该值的第二个副本。
  • 引用值是对象,存储在堆内存上。
  • 包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身。
  • 从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。
  • typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型。任何变量(不管包含的是原始值还是引用值)都存在于某个执 行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结如下。
  • 执行上下文分全局上下文、函数上下文和块级上下文。
  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。
  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
  • 变量的执行上下文用于确定什么时候释放内存。JavaScript 是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。JavaScript 的垃圾回收程序可以总结如下。
  • 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
  • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
  • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算
    法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对
    象(如 DOM 元素)。
  • 引用计数在代码中存在循环引用时会出现问题。
  • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对
    象、全局对象的属性和循环引用都应该在不需要时解除引用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值