前言
已经到了 2023 年了, 2022 着实是混了一年, 希望 2023 这一年可以每天尽量有一些技术上的输出。
计划是本月再把 javaScript 中的一些知识点在去复习一下, 复习也是获取新知识。一些太过于基础的就不多总结了。Let's go !
script 元素
将 JavaScript 插入 HTML 的主要方法是使用 <script> 元素。<script> 元素有下 列 8 个属性。
- async:表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其 他脚本加载。
- charset:使用 src 属性指定的代码字符集。这个属性很少使用,因为大多数浏览器不在乎它的值。
- crossorigin:配置相关请求的CORS(跨源资源共享)设置。
- defer:表示脚本可以延迟到文档完全被解析和显示之后再执行。
- integrity:允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI, Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错, 脚本不会执行。
- src:表示包含要执行的代码的外部文件。
- type:代替 language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。如果这个值是 module,则代 码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字。
还有一个 language 废弃了,就不再赘述了。
看似简单的属性,其实则考察对浏览器的渲染是否熟悉。
做一个知识的搬运工,以下几篇文章解释了 async 和 defer 的作用及其区别。
www.shuzhiduo.com/A/x9J2DPZNd… async 和 defer 的作用及其区别。
jinlong.github.io/2017/05/08/… 浏览器渲染做了什么?
语言基础
只总结一些比较细节的知识点了。
var 关键字
一说到 var 第一时间想到的就是变量提升。那么为什么会出现变量提升呢? 先来看看js代码执行流程。
代码执行分为编译阶段和执行阶段(当执行全局代码 遇到函数时,也会先对函数进行编译,然后再执行)
编译阶段: js代码经过编译后,会生成两部分内容:执行上下文和可执行代码。
创建执行上下文由三部分组成:即This Binding、VariableEnvironment(变量环境组件)、LexicalEnvironment(词法环境组件)。
- This Binding即this的指向。
- 变量环境组件VariableEnvironment 保存着 var声明的变量和函数function、并且变量初始化值为undefined。 词法环境组件Lexical Environment 保存着let、const声明的变量和函数。
- js引擎会把声明以外的代码编译为字节码,作为可执行代码。。
执行阶段: 执行代码时,遇到变量或者函数,会到执行上下文所保存的变量环境组件VariableEnvironment 或者LexicalEnvironment词法环境组件查找。
变量提升的原因是js代码在编译过程中已经对var声明的变量和function函数进行初始化。所以变量提升的本质,其实只是变量创建的过程 和 真实赋值的过程不同步带来的错觉。
let、const声明的变量也会进行变量提升,但是会形成暂时性死区,所以在执行到赋值代码之前,访问变量的话会报错。
let 声明
与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声 明的变量则会)
let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域, 而 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 没有定义
自从ES6 出现之后,let 声明就已经替代了var声明了,在日常开发中已经很少见到var声明。
const 声明
const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且 尝试修改 const 声明的变量会导致运行时错误。
const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象, 那么修改这个对象内部的属性并不违反 const 的限制。
使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不 合法的赋值操作。因此,很多开发者认为应该优先使用 const 来声明变量,只在提前知道未来会有修改时,再使用 let。这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因 意外赋值导致的非预期行为。
数据类型
Null 类型
Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因
undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等,如下面的例 子所示:
console.log(null == undefined); // true
Number 类型
ECMAScript 中最有意思的数据类型或许就是 Number 了。Number 类型使用 IEEE 754 格式表示整 数和浮点值(在某些语言中也叫双精度值)。
整数也可以用八进制(以 8 为基数)或十六进制(以 16 为基数)字面量表示。对于八进制字面量, 第一个数字必须是零(0),然后是相应的八进制数字(数值 0~7)。如果字面量中包含的数字超出了应 有的范围,就会忽略前缀的零,
let octalNum1 = 070; // 八进制的 56
let octalNum2 = 079; // 无效的八进制值,当成 79 处理
let octalNum3 = 08; // 无效的八进制值,当成 8 处理
要创建十六进制字面量,必须让真正的数值前缀 0x(区分大小写),然后是十六进制数字(09 以 及 AF)。十六进制数字中的字母大小写均可。
let hexNum1 = 0xA; // 十六进制 10
let hexNum2 = 0x1f; // 十六进制 31
因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为 整数。在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小 数点后面跟着 0(如 1.0),那它也会被转换为整数
需要注意的是, js中由于能存储最大精度的限制导致计算有误, 比如 0.1 + 0.2 就不等于 0.3 可以看一下这篇文章的解释 详解js中0.1+0.2!=0.3
注意 之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript 所独有。其他使用相同格式的语言也有这个问题。
数值转换
有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是 转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个 函数执行的操作也不同。
Number()函数基于如下规则执行转换。
-
布尔值,true 转换为 1,false 转换为 0。
-
数值,直接返回。
-
null,返回 0。
-
undefined,返回 NaN。
-
字符串,应用以下规则。
-
如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。 因此,Number("1")返回 1,Number("123")返回 123,Number("011")返回 11(忽略前面 的零)
如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
如果是空字符串(不包含字符),则返回 0。
如果字符串包含除上述情况之外的其他字符,则返回 NaN。
对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换
let num1 = Number("Hello world!"); // NaN let num2 = Number(""); // 0 let num3 = Number("000011"); // 11 let num4 = Number(true); // 1parseInt()函数更专注于字符串是否包含数值模式。字符串最前面的空格会被 忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即 返回 NaN。
let num1 = parseInt("1234blue"); // 1234 let num2 = parseInt(""); // NaN let num3 = parseInt("0xA"); // 10,解释为十六进制整数 let num4 = parseInt(22.5); // 22 let num5 = parseInt("70"); // 70,解释为十进制值 let num6 = parseInt("0xf"); // 15,解释为十六进制整数parseFloat()函数的工作方式跟 parseInt()函数类似,都是从位置 0 开始检测每个字符。同样, 它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有 效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5"将转换 成 22.34。
let num1 = parseFloat("1234blue"); // 1234,按整数解析 let num2 = parseFloat("0xA"); // 0 let num3 = parseFloat("22.5"); // 22.5 let num4 = parseFloat("22.34.5"); // 22.34 let num5 = parseFloat("0908.5"); // 908.5 let num6 = parseFloat("3.125e7"); // 31250000
Symbol 类型
Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为 Object API 提供了方法,可以更方便地发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非 字符串形式的对象属性
符号需要使用 Symbol()函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回 symbol。
let sym = Symbol();
console.log(typeof sym); // symbol
最重要的是,Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符 号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原 始值的包装对象:
最重要的是,Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符 号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原 始值的包装对象:
使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册 表中创建并重用符号。
为此,需要使用 Symbol.for()方法:
let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol
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.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字 符串键。如果查询的不是全局符号,则返回 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
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和 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';
Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val}
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果 没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
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)
常用内置符号
ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者 可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道 for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义 Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。
这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号 的实例。所有内置符号属性都是不可写、不可枚举、不可配置的
注意 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如, @@iterator 指的就是 Symbol.iterator。
Symbol.iterator
for-of 循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以 Symbol.iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API 的 Generator:
class Foo {
*[Symbol.iterator]() {}
}
let f = new Foo();
console.log(f[Symbol.iterator]()); // Generator {}
技术上,这个由 Symbol.iterator 函数生成的对象应该通过其 next()方法陆续返回值。可以通 过显式地调用 next()方法返回,也可以隐式地通过生成器函数返回
class Emitter {
constructor(max) {
this.max = max;
this.idx = 0;
}
*[Symbol.iterator]() {
while(this.idx < this.max)
{ yield this.idx++;
}
}
}
count(); // 0 1 2 3 4