一. 前言
[图: Copyright reserved by Mozilla]
JavaScript 的核心语言是 ECMAScript, 是一门由 ECMA TC39 委员会标准化的编程语言. " ECMAScript " 是语言标准的术语, 但" ECMAScript " 和" JavaScript " 是可以互换使用的.
个人相对青睐于JavaScript的语法结构和代码风格, 相对于Python.
1.1 代码运行环境
本文的所有代码执行环境: 浏览器(Chrome86, Edege115
) 和 Nodejs16
.
使用的JavaScript
标准版本为 ES6+
.
> {
... let a = 2;
... let b = (3 * a++) + a;
... // a++ 表示, 先执行 3 * a(2), 然后进行 a = a + 1, a 发生了变化
... // 执行顺序 3 * 2 + 3 = 9
... console.log(b);
... let c = 2;
... let d = c + (3 * c++);
... // 执行顺序: 2 + 3 * 2
... console.log(d);
... }
9
8
在nodejs
中执行:
在chrome
控制板中执行:
需要注意, 使用浏览器 dev console
行代码时, 默认 use strict
是不用的.
或者在代码片段块中执行:
二. 基本
2.1 数据类型
在 JavaScript
中有 6 种不同的数据类型:
- string
- number
- boolean
- object
- function
- symbol
3 种对象类型:
- Object(Function也是对象)
- Date
- Array
2 个不包含任何值的数据类型:
- null
- undefined
注意JavaScript
是弱类型语言, 执行数值运算需要注意.
// 数值运算, 当运算的数据其中存在非数字, js会对数据进行隐式的转换
typeof 5 + '4'
"number4"
typeof '4' + 5
"string5"
console.log('5' + 4);
54
console.log(4 + '5');
45
console.log(5 - '4');
1
1 == '1';
true
// 严格判断
1 === '1';
false
// 各种奇怪的隐式转换
> {} + [];
0
> [] + {};
'[object Object]'
更多这种类型的数据间的对比见: JS Comparison Table (dorey.github.io).
2.1.1 Symbol
需要注意的是symbol
类型, 这是ES6
新增feature
.
symbol 是一种基本数据类型( primitive data type) .
Symbol()
函数会返回 symbol 类型的值, 该类型具有静态属性和静态方法. 它的静态属性会暴露几个内建的成员对象; 它的静态方法会暴露全局的 symbol 注册, 且类似于内建对象类, 但作为构造函数来说它并不完整, 因为它不支持语法: "new Symbol()
".
> {
... const private_a = Symbol();
... const dic = {
// 外部将无法访问该属性
... [private_a]: 'password',
... test(){console.log(this[private_a])}
... };
... dic.test();
// json转字符串, 该属性也会被剔除掉
... console.log(JSON.stringify(dic));
... }
password
{}
Symbols 在
for...in
迭代中不可枚举. 另外,Object.getOwnPropertyNames()
不会返回 symbol 对象的属性, 但是你能使用Object.getOwnPropertySymbols()
得到它们.
{
var obj = {};
obj[Symbol("a")] = "a";
obj[Symbol.for("b")] = "b";
obj["c"] = "c";
obj.d = "d";
for (var i in obj) {
console.log(i); // logs "c" and "d"
}
}
Symbol.iterator
为每一个对象定义了默认的迭代器. 该迭代器可以被for...of
循环使用.
// 这种写法比较难看, 改版的代码见 生成器部分章节
> {
... let range = {
... from: 1,
... to: 5,
... [Symbol.iterator]() {
... return {
... current: this.from,
... last: this.to,
... next() {
... return this.current < this.last ? { done: false, value: this.current++ }: { done: true };
... }
... };
... }
... };
...
... for(let value of range) console.log(value);
... }
1
2
3
4
2.1.2 判断数据类型
常用的三种判断数据的方法, typeof
用于简单数据类型的判断; instanceof
, 用于判断数据是否为某对象的实例;Object.prototype.toString.call
精确判断绝大部分的数据类型.
-
typeof
// 无法判断具体的类型, 当对象为Object时 typeof 'abc'; "string" typeof []; "object" typeof {}; "object"
-
instanceof
> [1,2,3] instanceof Array; true > new Date() instanceof Date; true > new Function() instanceof Function; true > null instanceof Object; false
-
Object.prototype.toString.call
, 最有效的判断方式.> console.log(Object.prototype.toString.call("123")) [object String] > console.log(Object.prototype.toString.call(123)) [object Number] > console.log(Object.prototype.toString.call(true)) [object Boolean] > console.log(Object.prototype.toString.call([1, 2, 3])) [object Array] > console.log(Object.prototype.toString.call(null)) [object Null] > console.log(Object.prototype.toString.call(undefined)) [object Undefined] > console.log(Object.prototype.toString.call({name: 'Hello'})) [object Object] > console.log(Object.prototype.toString.call(function () {})) [object Function] > console.log(Object.prototype.toString.call(new Date())) [object Date]
2.2 变量声明
声明变量的三种方式, var, let, const
, 简单理解, 后二者是block
(块)变量声明的方式.
// 同时声明多个变量
{
const a = 1, b = 2, c=3;
b = 4; // error
}
JavaScript 变量生命周期在它声明时初始化.
局部(block)变量在函数执行完毕后销毁.
全局(global)变量在页面关闭后销毁.
Node.js中, 一个.js文件就是一个完整的作用域(module,模块). 因此 var 声明的变量只在当前.js文件中有效,而不是全局有效. 而global全局对象是独立于所有的.js(module,模块)之上的.
let, const
, 模块级别变量声明, 没什么奇特的, 但是需要注意var
和前两者的差异.
var
声明变量会自动提升
> {
// var声明变量会自动提升
... console.log(b); // b访问前尚未声明, 但是没有出现报错. 严格模式下也没报错
... const a = 1;
... console.log(a);
... var b = 2;
... }
1
undefined
// 函数访问外部的变量
{
function test(a){
console.log(a + b)
}
let b = 2;
test(1);
}
VM65:3 3
{
function test(a){
console.log(a + b)
}
// let b = 2;将变量放置于此可以执行
test(1);
let b = 2; // error
}
VM71:3 Uncaught ReferenceError: Cannot access 'b' before initialization
at test (<anonymous>:3:25)
{
function test(a){
console.log(a + b)
}
test(1); //访问前未声明变量
var b = 2;
}
VM77:3 NaN
var
可以实现全局对象的绑定, 在浏览器中常见访问window.variant
访问全局的对象.
> var foo = 'global';
> let bar = 'global';
>
> console.log(this.foo) // global
global
> console.log(this.bar) // undefined
全局变量访问差异.
注意: 虽然以下段代码可以在浏览器中正常运行, 但在 Node.js 中 f1()
会产生一个" 找不到变量 x
" 的 ReferenceError
. 这是因为在 Node 中顶级作用域不是全局作用域, 而 x
其实是在当前模块的作用域之中.
var a = 1;
var a = 2;
console.log(a) // 2
function test() {
var a = 3;
var a = 4;
console.log(a) // 4
}
test()
VM254:4 2
VM254:9 4
> {
... let i = 10;
... var m = 10;
... const a = () => {
... let i = 0;
... console.log(i);
... var m = 1;
... console.log(m);
... }
... a();
... }
0
1
> {
... let i = 10;
... var m = 10;
... var m = 1;
... console.log(m);
... let i = 0;
let i = 0;
^
Uncaught SyntaxError: Identifier 'i' has already been declared
> console.log(i);
Uncaught ReferenceError: i is not defined
var
允许在同一层级反复声明, let, const
只允许声明一次.
// 当var声明的变量和函数同名
{
var test = 1;
console.log(test);
function test(){
console.log('test');
}
test();
}
Uncaught SyntaxError: Identifier 'test' has already been declared
{
function test(){
console.log('test');
}
test();
var test = () => console.log('arrow');
}
Uncaught SyntaxError: Identifier 'test' has already been declared
let, const
作用于局部最为直观的好处, 更好的控制变量的作用范围(避免污染).
> {
// i全部指向 3, 即最后一个值
... for(var i = 0; i<3; i++) setTimeout(()=> console.log(i), 100);
... }
> 3
3
3
> {
//每次执行的settimeout, => 生成新的变量 i
... for(let i = 0; i<3; i++) setTimeout(()=> console.log(i), 100);
... }
> 0
1
2
2.3 数组
JavaScript
的数组实际上就是python
中的list
, 数组(Array
)这个命名并不是很好, 因为TypedArray - JavaScript | MDN (mozilla.org)这种底层数组没有和普通数组在名字上形成明显的差异(区分, 最起码在表述上容易搞混).
2.3.1 for of/in
in
是一个独立的运算符.
如果指定的属性在指定的对象或其原型链中, 则 in
运算符返回 true
.
// index
1 in [2, 4,7]
true
3 in [2, 4,7]
false
7 in [2, 4,7]
false
// key
'a' in {'b':1, 'a': 2}
true
'a' in {'b':1, 'v': "a"}
false
for...in
语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性, 包括继承的可枚举属性.
for...of
语句在可迭代对象( 包括Array
,Map
,Set
,String
,TypedArray
, arguments 对象等等) 上创建一个迭代循环, 调用自定义迭代钩子, 并为每个不同属性的值执行语句
> {
... const dic = {
... 'a': 1,
... 'b': 2
... }
... for (const e in dic) console.log(e);
... const a = [3,4,5];
// 打印出来的并不是值, 而是index
... for (const c in a) console.log(c);
... }
a
b
0
1
2
> {
... const dic = {
... 'a': 1,
... 'b': 2
... }
... const a = [3,4,5];
... for (const c of a) console.log(c);
... for (const e of dic) console.log(e);
... }
3
4
5
Uncaught TypeError: dic is not iterable
其他的可迭代对象
for (const a in 'abc') console.log(a);
VM177:1 0
VM177:1 1
VM177:1 2
/*
和python的类似
for e in 'abc':
print(e)
*/
for (const a of 'abc') console.log(a);
VM199:1 a
VM199:1 b
VM199:1 c
[...'abc'].forEach((e)=>console.log(e));
VM189:1 a
VM189:1 b
VM189:1 c
2.3.2 map/filter/reduce
> [1,2,3].map((e)=>e>1);
[ false, true, true ]
> [1,2,3].filter((e)=>e>1);
[ 2, 3 ]
> [1,2,3].reduce((s,e) => s+=e);
6
相对特殊的是reduce()
函数, 可以额外传递一个初始值.
reduce(callbackFn, initialValue)
> [1,2,3].reduce((s,e) => s+=e, 10);
16
2.4 日期
方法 | 描述 |
---|---|
getDate() | 从 Date 对象返回一个月中的某一天 (1 ~ 31). |
getDay() | 从 Date 对象返回一周中的某一天 (0 ~ 6). |
getFullYear() | 从 Date 对象以四位数字返回年份. |
getHours() | 返回 Date 对象的小时 (0 ~ 23). |
getMilliseconds() | 返回 Date 对象的毫秒(0 ~ 999). |
getMinutes() | 返回 Date 对象的分钟 (0 ~ 59). |
getMonth() | 从 Date 对象返回月份 (0 ~ 11). |
getSeconds() | 返回 Date 对象的秒数 (0 ~ 59). |
getTime() | 返回 1970 年 1 月 1 日至今的毫秒数. |
getTimezoneOffset() | 返回本地时间与格林威治标准时间 (GMT) 的分钟差. |
getUTCDate() | 根据世界时从 Date 对象返回月中的一天 (1 ~ 31). |
getUTCDay() | 根据世界时从 Date 对象返回周中的一天 (0 ~ 6). |
getUTCFullYear() | 根据世界时从 Date 对象返回四位数的年份. |
getUTCHours() | 根据世界时返回 Date 对象的小时 (0 ~ 23). |
getUTCMilliseconds() | 根据世界时返回 Date 对象的毫秒(0 ~ 999). |
getUTCMinutes() | 根据世界时返回 Date 对象的分钟 (0 ~ 59). |
getUTCMonth() | 根据世界时从 Date 对象返回月份 (0 ~ 11). |
getUTCSeconds() | 根据世界时返回 Date 对象的秒钟 (0 ~ 59). |
parse() | 返回1970年1月1日午夜到指定日期( 字符串) 的毫秒数. |
setDate() | 设置 Date 对象中月的某一天 (1 ~ 31). |
setFullYear() | 设置 Date 对象中的年份( 四位数字) . |
setHours() | 设置 Date 对象中的小时 (0 ~ 23). |
setMilliseconds() | 设置 Date 对象中的毫秒 (0 ~ 999). |
setMinutes() | 设置 Date 对象中的分钟 (0 ~ 59). |
setMonth() | 设置 Date 对象中月份 (0 ~ 11). |
setSeconds() | 设置 Date 对象中的秒钟 (0 ~ 59). |
setTime() | setTime() 方法以毫秒设置 Date 对象. |
setUTCDate() | 根据世界时设置 Date 对象中月份的一天 (1 ~ 31). |
setUTCFullYear() | 根据世界时设置 Date 对象中的年份( 四位数字) . |
setUTCHours() | 根据世界时设置 Date 对象中的小时 (0 ~ 23). |
setUTCMilliseconds() | 根据世界时设置 Date 对象中的毫秒 (0 ~ 999). |
setUTCMinutes() | 根据世界时设置 Date 对象中的分钟 (0 ~ 59). |
setUTCMonth() | 根据世界时设置 Date 对象中的月份 (0 ~ 11). |
setUTCSeconds() | setUTCSeconds() 方法用于根据世界时 (UTC) 设置指定时间的秒字段. |
toDateString() | 把 Date 对象的日期部分转换为字符串. |
toISOString() | 使用 ISO 标准返回字符串的日期格式. |
toJSON() | 以 JSON 数据格式返回日期字符串. |
toLocaleDateString() | 根据本地时间格式, 把 Date 对象的日期部分转换为字符串. |
toLocaleTimeString() | 根据本地时间格式, 把 Date 对象的时间部分转换为字符串. |
toLocaleString() | 根据本地时间格式, 把 Date 对象转换为字符串. |
toString() | 把 Date 对象转换为字符串. |
toTimeString() | 把 Date 对象的时间部分转换为字符串. |
toUTCString() | 根据世界时, 把 Date 对象转换为字符串. 实例: var today = new Date(); var UTCstring = today.toUTCString(); |
UTC() | 根据世界时返回 1970 年 1 月 1 日 到指定日期的毫秒数. |
valueOf() | 返回 Date 对象的原始值. |
2.5 正则表达式
注意Python
中的正则表达式和Javascript
的存在大量的差异, 相关内容见: Python正则表达式 -" \w "-坑 | Lian (kyouichirou.github.io).
修饰符 | 描述 |
---|---|
i | 执行对大小写不敏感的匹配. |
g | 执行全局匹配( 查找所有匹配而非在找到第一个匹配后停止) . |
m | 执行多行匹配. |
方法 | 描述 |
---|---|
search | 检索与正则表达式相匹配的值. |
match | 找到一个或多个正则表达式的匹配. |
replace | 替换与正则表达式匹配的子串. |
split | 把字符串分割为字符串数组. |
2.6 全局函数
函数 | 描述 |
---|---|
decodeURI() | 解码某个编码的 URI. |
decodeURIComponent() | 解码一个编码的 URI 组件. |
encodeURI() | 把字符串编码为 URI. |
encodeURIComponent() | 把字符串编码为 URI 组件. |
escape() | 对字符串进行编码. |
eval() | 计算 JavaScript 字符串, 并把它作为脚本代码来执行. |
isFinite() | 检查某个值是否为有穷大的数. |
isNaN() | 检查某个值是否是数字. |
Number() | 把对象的值转换为数字. |
parseFloat() | 解析一个字符串并返回一个浮点数. |
parseInt() | 解析一个字符串并返回一个整数. |
String() | 把对象的值转换为字符串. |
unescape() | 对由 escape() 编码的字符串进行解码. |
2.7 参数
2.7.1 位置和默认参数
> {
... const f = (a, b='hello') => console.log(a + "|" + b);
... f('alex');
... // 前后放置默认参数的变量, 并不会报错.
... const fa = (a='nice', b) => console.log(a + "|" + b);
... fa(b='alex');
... }
alex|hello
alex|undefined
2.7.2 arguments
arguments
is an array-like object accessible inside functions that contains the values of the arguments passed to that function.
> {
... function test(a, b, c){
... console.log(arguments);
... }
... test(1, 2, 3);
... }
[Arguments] { '0': 1, '1': 2, '2': 3 }
> {
... function test(a, b, c){
... console.log(arguments[0]);
... }
... test(1, 2, 3);
... }
1
但是需要注意箭头函数是不支持arguments
> {
... const test = (a, b, c) => console.log(arguments);
... test();
... }
Uncaught ReferenceError: arguments is not defined
at test (REPL327:2:43)
2.7.3 参数解包
> {
... const my_sum = (...args) => console.log(args.reduce((e, i) => i += e));
... my_sum(1,2,3);
... my_sum([1,2,3]);
... my_sum([1,2], 3);
... }
6
[ 1, 2, 3 ]
31,2
undefined
> {
... const my_sum = (...args) => console.log(args);
... my_sum(1,2,3);
... my_sum([1,2,3]);
... my_sum([1,2], 3);
... }
[ 1, 2, 3 ]
[ [ 1, 2, 3 ] ]
[ [ 1, 2 ], 3 ]
2.8 模板字符串
const date = '2023';
const s = `
this is a module string.
${date}
`;
模板字符串, 在python
中
variant = 'hello'
s = f"this is a module string {variant}"
2.9 深/浅拷贝
> {
// 需要区分这种
... const a = {'a': [1,2,3], 'b': 4};
... const b = a;
... a.b = 5;
... console.log(a);
... console.log(b);
... }
{ a: [ 1, 2, 3 ], b: 5 }
{ a: [ 1, 2, 3 ], b: 5 }
> {
... const a = [1,2,3];
... const b = a;
... a[0] = 4;
... console.log(a);
... console.log(b);
... }
[ 4, 2, 3 ]
[ 4, 2, 3 ]
> {
... const a = [1,2,3];
... let b = a; // a, b 指向地址address x,
... // b[1] = 10; // 单个元素修改, 相当于在 x 上进行
... b = [3,4,5]; // 注意这两种修改的差异, 相当于将b的地址指向 address y
... console.log(a);
... console.log(b);
... }
[ 1, 2, 3 ]
[ 3, 4, 5 ]
> {
... const a = [ 'a', {'b': [1,2]}];
... const b = a;
... console.log(a === b); // a和b是一样的东西
... const c = Array.from(a); // 浅复制, a和c不是一样东西
... console.log(c === a);
... }
true
false
> {
... const a = ['a', { 'b': [1, 2] }];
... const b = a;
... const c = Array.from(a);
... console.log(Object.is(a, b));
... console.log(Object.is(a, c));
... }
true
false
> {
... const a = [1,2,3];
... const b = JSON.parse(JSON.stringify(a));
... a[0] = 4;
... console.log(a);
... console.log(b);
... }
[ 4, 2, 3 ]
[ 1, 2, 3 ]
2.9.1 浅拷贝
对象的浅拷贝是其属性与拷贝源对象的属性共享相同引用( 指向相同的底层值) 的副本. 因此, 当你更改源或副本时, 也可能导致其他对象也发生更改- - 也就是说, 你可能会无意中对源或副本造成意料之外的更改. 这种行为与深拷贝的行为形成对比, 在深拷贝中, 源和副本是完全独立的.
> {
... const a = [ 'a', {'b': [1,2]}];
... const b = Array.from(a); // 浅拷贝
... a[0] = 'c'; // 修改第一个值
... console.log(b);
... }
[ 'a', { b: [ 1, 2 ] } ] // b的内容并没有改变
undefined
> {
... const a = [ 'a', {'b': [1,2]}];
... const b = Array.from(a);
... a[1].b = [2, 3]; // 修改值, b的也发生改变了
... console.log(b);
... }
[ 'a', { b: [ 2, 3 ] } ]
> {
... const a = [ 'a', {'b': [1,2]}];
... const b = Array.from(a);
... a[1].b[0] = 4; // 修改单个值, b也发生改变
... console.log(b);
... }
[ 'a', { b: [ 4, 2 ] } ]
2.9.2 深拷贝
对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用( 指向相同的底层值) 的副本. 因此, 当你更改源或副本时, 可以确保不会导致其他对象也发生更改; 也就是说, 你不会无意中对源或副本造成意料之外的更改. 这种行为与浅拷贝的行为形成对比, 在浅拷贝中, 对源或副本的更改可能也会导致其他对象的更改( 因为两个对象共享相同的引用) .
> {
... const a = [ 'a', {'b': [1,2]}];
... const b = JSON.parse(JSON.stringify(a));
... a[1].b[0] = 4;
... console.log(b);
... }
[ 'a', { b: [ 1, 2 ] } ]
深拷贝, 比较简单的实现就是使用json
作为中转媒介.
2.10 严格模式
ECMAScript 5的严格模式是采用具有限制性 JavaScript 变体的一种方式, 从而使代码隐式地脱离" 马虎模式/稀松模式/懒散模式" ( sloppy) 模式. 严格模式不仅仅是一个子集: 它的产生是为了形成与正常代码不同的语义. 不支持严格模式与支持严格模式的浏览器在执行严格模式代码时会采用不同行为. 所以在没有对运行环境展开特性测试来验证对于严格模式相关方面支持的情况下, 就算采用了严格模式也不一定会取得预期效果. 严格模式代码和非严格模式代码可以共存, 因此项目脚本可以渐进式地采用严格模式. 严格模式对正常的 JavaScript 语义做了一些更改.
- 严格模式通过抛出错误来消除了一些原有静默错误.
- 严格模式修复了一些导致 JavaScript 引擎难以执行优化的缺陷: 有时候, 相同的代码, 严格模式可以比非严格模式下运行得更快.
- 严格模式禁用了在 ECMAScript 的未来版本中可能会定义的一些语法.
在严格模式下,
for...in
循环头的var
声明中的初始化器被弃用并产生语法错误. 在非严格模式下, 它们会被静默地忽略.
> {
... function test(){
... 'use strict';
... const a = {'a': 1, 'b': 2};
... Object.defineProperty(a, 'a', {
... value: 20,
... writable: false,
... configurable: false,
... enumerable: false
... });
... a.a = 40;
... console.log(a.a);
... }
... test();
... }
// 这个不可写错误, 只有在严格模式下才会触发
Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'
at test (REPL234:11:13)
{
function foo() {
// "use strict";
console.log(this.a); // 严格模式, this指向 undefined
}
var a = 2;
foo();
}
VM333:4 Uncaught TypeError: Cannot read property 'a' of undefined
at foo (<anonymous>:4:27)
at <anonymous>:7:9
{
// 类似的情况
function foo() {
// "use strict";
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
}
2.11 Map/Set
// map, 对字典的补充
> {
... const dic = {1:"a"};
... console.log(dic[1]);
... console.log(dic['1']); // 不区分数字和字符串
...
... const map = new Map();
... map.set(1, 'a');
... console.log(map.get(1)); //区分数字
... console.log(map.get('1'));
... }
a
a
a
undefined
> {
... const dic = {"a": 1}
... const map = new Map();
... map.set(dic, 'a'); // key 支持 object
... console.log(map.get(dic));
... }
a
// 去重
> {
... const duplicate_remove = (data) => [...new Set(data)];
... console.log(duplicate_remove([1,2,3,1,5,3]));
... }
[ 1, 2, 3, 5 ]
2.12 尾递归优化
tail call optimization
, tco.
在递归的过程中, 需要占用内存的调用栈会越来越多, 那么如果是一个无线递归的函数, 势必会面临栈溢出的问题, 当然了即便不是无限的, 层数太多的话也会面临堆栈溢出的问题.
解决递归调用栈溢出的方法是通过尾递归优化或循环调用, 事实上尾递归和循环的效果是一样的, 所以, 把循环看成是一种特殊的尾递归函数也是可以的.
尾递归是指, 在函数返回的时候, 调用自身本身, 并且, return语句不能包含表达式. 这样, 编译器或者解释器就可以把尾递归做优化, 使递归本身无论调用多少次, 都只占用一个栈帧, 不会出现栈溢出的情况.
和Python
不一样, JavaScript
是支持尾递归优化的.
> {
... function test(num) {
... return num < 1 ? 0 : num + test(num - 1)
... }
... console.log(test(10000));
... }
50005000
>>> def test(num):
... return num < 1 if 0 else num + test(num - 1)
...
>>> print(test(10000))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in test
File "<stdin>", line 2, in test
File "<stdin>", line 2, in test
[Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
2.13 Style
相比于Python
这种更为接近"自然语言"的语言, JavaScript
虽然不断的容纳其他语言的优点, 但是JavaScript
还是相对在风格上接近于C
, 大量使用符号而不是关键词来执行一些操作.
// 三元运算符在python中使用if else实现
a = 1 if 5 > 4 else 0
// 当多条件
b = 1 if 5 > 6 else 2 if 4 > 3 else 0
// 使用符号来实现
const a = 5 > 4 ? 1 : 0;
const b = 5 > 6 ? 1 : 4 > 3 ? 2 : 0;
显然python
的代码更为容易阅读, JavaScript
的代码更cool
, 但是阅读起来却是痛苦的.
2.13.1 单行代码
假如阅读过JavaScript Snippets - 30 seconds of code, 各种一行代码, 就可以感受到JavaScript
的独特魅力, js
是很酷的.
// for
for (const e of 'abc') console.log(e);
//if
if (true)console.log(true);
else console.log(false);
// 声明变量
const a = 1, b = 2;
> {
// 将数组进行扁平化操作
... const deepFlatten = arr => [].concat(...arr.map(v => Array.isArray(v) ? deepFlatten(v) : v));
... console.log(deepFlatten([1,[2],[[3],4],5]));
... }
[ 1, 2, 3, 4, 5 ]
2.13.2 三元运算符号
这没什么好说的, 在各种语言中最为常见的一种操作.
const abc = 1;
abc === 0 ? 'abc' : abc === 2 ? 'bbc' : abc === 1 ? 'nfc' : "others";
'nfc'
2.13.3 可选链
这是为了方便判断对象是否有某个属性, 然后继续访问下一层, 如此往复, 相比于三元运算符的操作, 可选链操作符的操作更为简单方便.
> {
... const dic = {
... 'abc': {
... 'bbc': () => console.log('bbc')
... }
... };
... dic?.abc?.bbc();
... }
bbc
> {
... const dic = {
... 'abc': {
... 'bbc': () => console.log('bbc')
... }
... };
... dic.abc ? dic.abc.bbc ? dic.abc.bbc():null: null;
... }
bbc
2.13.4 空值合并运算符
??
, 注意和 ||
(or)的差异
> {
... const a = 0;
... const b = null; // undefined
... console.log(a ?? 'nothing');
... console.log(a || 'nothing');
...
...
... console.log(b ?? 'nothing');
... console.log(b || 'nothing');
... }
0
nothing
nothing
nothing
null, undefined, 二者等价, ??
主要解决 ||
(or)无法 0, false
等值的问题.
function config(options) {
options.duration ??= 100;
options.speed ??= 25;
return options;
}
config({ duration: 125 }); // { duration: 125, speed: 25 }
config({}); // { duration: 100, speed: 25 }
2.13.5 字典取值
> {
// 字典取值
... const {a, b} = {'a': 1, "c": 5, "b":3};
... console.log(a + "|" + b);
... }
1|3
2.13.6 数组取值
在python
中支持一种非常方便高效的取值方式:
>>> a, b = 1, 3
>>> a
1
>>> b
3
>>> a, b, c = [1,2,3]
>>> a
1
>>> b
2
>>> c
3
但是JavaScript
的逗号充当:
逗号(
,
) 运算符对它的每个操作数从左到右求值, 并返回最后一个操作数的值.
> {
... let a = 1, b = 2;
... //a, b = 30, 40;
// 等价于
... a,
... b = 30,
... 40;
... console.log(a);
... console.log(b);
... }
1
30
> {
... let a = 1, b = 2;
// 需要借助[], 实现多个元素的取值
... [a,b] = [30,40];
... console.log(a);
... console.log(b);
... }
30
40
2.13.7 三点运算符
> {
... const a = [1,2,3];
// 解包
... console.log(Math.max(...a));
// 合并
... console.log([...a, 4]);
...
... const s = 'abc';
... console.log([...s]);
... console.log(...s);
...
... const ab = {id:1};
... const bb = {id:2, f(...args){console.log(this.id + "|" + args.join(','))}};
... bb.f.apply(ab, [...s]);
... // 不定长参数
... const my_sum = (...args) => console.log(args.reduce((e, i) => i += e));
... my_sum(1,2,3);
... }
3
[ 1, 2, 3, 4 ]
[ 'a', 'b', 'c' ]
a b c
1|a,b,c
6
> {
// 分拆
... const [a, ...b] = [1,2,3,4];
... console.log(a);
... console.log(b);
... }
1
[ 2, 3, 4 ]
在python
中的实现类似的funcion
是星号( * )
符号.
>>> a = [1,2,3]
>>> print([*a, 4])
[1, 2, 3, 4]
>>> f = lambda *args: sum(args)
>>> print(f(1,2,3))
6
>>> print(f(1,2))
3
>>>
>>> a, *b = [1, 2,3,4]
>>> a
1
>>> b
[2, 3, 4]
2.13.8 其他
> {
... let b = [1, 2, 3];
... const a = () => console.log('a');
// 判断与执行
... b && a();
... }
a
2.13.9 小结
特定的风格的代码除了简化代码的书写和美观之外, 还需要注意代码的可阅读性.
2.14 iframe
iframe
作为曾今非常经典的一项技术, 当前尽管看似要淘汰(减少使用, 不鼓励使用), 但是在实现某些功能上, 如在A页面, 嵌入B页面的内容, iframe依然是简单易用的.
- Why are frames deprecated in html? - Stack Overflow
- 为什么说前端应该尽量少用 iframe? - 知乎 (zhihu.com)
- Iframe 有什么好处, 有什么坏处? 国内还有哪些知名网站仍用Iframe, 为什么? 有哪些原来用的现在抛弃了? 又是为什么? - 知乎 (zhihu.com)
2.15 存储
2.15.1 Storage
相比于纯字符串的cookie
, storage
是以键值的形式存储, 存储数据量更大, 读/存更为方便.
storage
分为两种, api
上是一致的.
localStorage.setItem('test', 'data');
- localStorage, 永久存储
- sessionStorage, 会话存储
2.15.2 IndexedDB
键值型数据库.
使用 IndexedDB 执行的操作是异步执行的, 以免阻塞应用程序. IndexedDB 最初包括同步和异步 API. 同步 API 仅用于 Web Workers, 且已从规范中移除, 因为尚不清晰是否需要. 但如果 Web 开发人员有足够的需求, 可以重新引入同步 API.
需要注意的是, 目前indexeddb
仅支持异步的模式.
IndexedDB 是一个事务型数据库系统, 类似于基于 SQL 的 RDBMS. 然而, 不像 RDBMS 使用固定列表, IndexedDB 是一个基于 JavaScript 的面向对象数据库. IndexedDB 允许您存储和检索用键索引的对象; 可以存储结构化克隆算法支持的任何对象. 您只需要指定数据库模式, 打开与数据库的连接, 然后检索和更新一系列事务.
// 简单封装一个示例
class Database {
/*
dname: name of database;
tname: name of table;
mode: read or read&write;
*/
constructor(dbname, tbname = "", rwmode = false, version = 1) {
this.dbopen =
version === 1
? indexedDB.open(dbname)
: indexedDB.open(dbname, version);
this.RWmode = rwmode ? "readwrite" : "readonly";
this.tbname = tbname;
const getIndex = (fieldname) => this.Table.index(fieldname);
}
Initialize() {
return new Promise((resolve, reject) => {
//if the db does not exist, this event will fired firstly;
//adjust the version of db, which can trigger this event => create/delete table or create/delete index (must lauch from this event);
this.dbopen.onupgradeneeded = (e) => {
this.store = e.target.result;
this.updateEvent = true;
this.storeEvent();
resolve(0);
};
this.dbopen.onsuccess = () => {
if (this.store) return;
this.updateEvent = false;
this.store = this.dbopen.result;
this.storeEvent();
resolve(1);
};
this.dbopen.onerror = (e) => {
console.log(e);
reject("error");
};
/*
The event handler for the blocked event.
This event is triggered when the upgradeneeded event should be triggered _
because of a version change but the database is still in use (i.e. not closed) somewhere,
even after the versionchange event was sent.
*/
this.dbopen.onblocked = () => {
console.log("please close others tab to update database");
reject("conflict");
};
this.dbopen.onversionchange = (e) =>
console.log("The version of this database has changed");
});
}
createTable(keyPath) {
if (this.updateEvent) {
const index = keyPath
? { keyPath: keyPath }
: { autoIncrement: true };
this.store.createObjectStore(this.tbname, index);
}
}
createNewTable(keyPath) {
return new Promise((resolve, reject) => {
this.version = this.store.version + 1;
this.store.close();
if (this.storeErr) {
reject("database generates some unknow error");
return;
}
this.dbopen = indexedDB.open(this.store.name, this.version);
this.Initialize().then(
() => {
this.createTable(keyPath);
resolve(true);
},
() => reject("database initial fail")
);
});
}
storeEvent() {
this.store.onclose = () => console.log("closing...");
this.store.onerror = () => (this.storeErr = true);
}
get checkTable() {
return this.store.objectStoreNames.contains(this.tbname);
}
get Tablenames() {
return this.store.objectStoreNames;
}
get DBname() {
return this.store.name;
}
get Indexnames() {
return this.Table.indexNames;
}
get DBversion() {
return this.store.version;
}
get Datacount() {
return new Promise((resolve, reject) => {
const req = this.Table.count();
req.onsuccess = (e) =>
resolve({
count: e.target.result,
name: e.target.source.name,
});
req.onerror = (e) => reject(e);
});
}
//take care the transaction, must make sure the transaction is alive when you need deal with something continually
get Table() {
const transaction = this.store.transaction(
[this.tbname],
this.RWmode
);
const table = transaction.objectStore(this.tbname);
transaction.onerror = () =>
console.log("warning, error on transaction");
return table;
}
rollback() {
this.transaction && this.transaction.abort();
}
read(keyPath) {
return new Promise((resolve, reject) => {
const request = this.Table.get(keyPath);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject("error");
});
}
batchCheck(tables, keyPath) {
return new Promise((resolve) => {
const arr = [];
for (const t of tables) {
const transaction = this.store.transaction(
this.store.objectStoreNames,
this.RWmode
);
const table = transaction.objectStore(t);
const rq = new Promise((reso, rej) => {
const req = table.get(keyPath);
req.onsuccess = () => reso(req.result);
req.onerror = () => rej("error");
});
arr.push(rq);
}
Promise.allSettled(arr).then((results) => {
resolve(
results.map((r) =>
r.status === "rejected" ? null : r.value
)
);
});
});
}
add(info) {
return new Promise((resolve, reject) => {
const op = this.Table.add(info);
op.onsuccess = () => resolve(true);
op.onerror = (e) => {
console.log(e);
reject("error");
};
});
}
getAll(tables) {
return new Promise((resolve) => {
const arr = [];
for (const t of tables) {
const transaction = this.store.transaction(
this.store.objectStoreNames,
this.RWmode
);
const table = transaction.objectStore(t);
const rq = new Promise((reso, rej) => {
const req = table.getAll();
req.onsuccess = (e) =>
reso({
name: e.target.source.name,
data: e.target.result,
});
req.onerror = () => rej(t);
});
arr.push(rq);
}
Promise.allSettled(arr).then((results) => resolve(results));
});
}
updateRecord(keyPath) {
return new Promise((resolve, reject) => {
this.read(keyPath).then(
(result) => {
if (result) {
result.visitTime = Date.now();
let times = result.visitTimes;
result.visitTimes = times ? ++times : 2;
this.update(result).then(
() => resolve(true),
(err) => reject(err)
);
} else resolve(true);
},
(err) => reject(err)
);
});
}
update(info, keyPath, mode = false) {
//if db has contained the item, will update the info; if it does not, a new item is added
return new Promise((resolve, reject) => {
//keep cursor
if (mode) {
this.read(info[keyPath]).then(
(result) => {
if (!result) {
this.add(info).then(
() => resolve(true),
(err) => reject(info)
);
} else {
const op = this.Table.put(
Object.assign(result, info)
);
op.onsuccess = () => resolve(true);
op.onerror = (e) => {
console.log(e);
reject(info);
};
}
},
(err) => console.log(err)
);
} else {
const op = this.Table.put(info);
op.onsuccess = () => resolve(true);
op.onerror = (e) => {
console.log(e);
reject(info);
};
}
});
}
clear() {
this.Table.clear();
}
//must have primary key
deleteiTems(keyPath) {
return new Promise((resolve, reject) => {
const op = this.Table.delete(keyPath);
op.onsuccess = () => {
console.log("delete item successfully");
resolve(true);
};
op.onerror = (e) => {
console.log(e);
reject("error");
};
});
}
//note: create a index must lauch from onupgradeneeded event; we need triggle update event
createIndex(indexName, keyPath, objectParameters) {
if (!this.updateEvent) {
console.log("this function must be through onupgradeneeded");
return;
}
this.table.createIndex(indexName, keyPath, objectParameters);
}
deleTable() {
if (!this.updateEvent) {
console.log("this function must be through onupgradeneeded");
return;
}
this.dbopen.deleteObjectStore(this.tbname);
}
deleIndex(indexName) {
if (!this.updateEvent) {
console.log("this function must be through onupgradeneeded");
return;
}
this.table.deleteIndex(indexName);
}
close() {
//The connection is not actually closed until all transactions created using this connection are complete
this.store.close();
}
static deleDB(dbname) {
return new Promise((resolve, reject) => {
const DBDeleteRequest = window.indexedDB.deleteDatabase(dbname);
DBDeleteRequest.onerror = (e) => {
console.log(e);
reject("error");
};
DBDeleteRequest.onsuccess = () => {
console.log("success");
resolve(true);
};
});
}
}
2.16 DOM相关
2.16.1 addEventListener
**备注: ** 推荐使用
addEventListener()
来注册一个事件监听器, 理由如下:
- 它允许为一个事件添加多个监听器. 特别是对库, JavaScript 模块和其他需要兼容第三方库/插件的代码来说, 这一功能很有用.
- 相比于
onXYZ
属性绑定来说, 它提供了一种更精细的手段来控制listener
的触发阶段. ( 即可以选择捕获或者冒泡) .- 它对任何事件都有效, 而不仅仅是 HTML 或 SVG 元素.
type
表示监听事件类型的大小写敏感的字符串.
listener
当所监听的事件类型触发时, 会接收到一个事件通知( 实现了
Event
接口的对象) 对象.listener
必须是一个实现了EventListener
接口的对象, 或者是一个函数. 有关回调本身的详细信息, 请参阅事件监听回调
options
可选一个指定有关
listener
属性的可选参数对象. 可用的选项如下:capture
可选一个布尔值, 表示listener
会在该类型的事件捕获阶段传播到该EventTarget
时触发.once
可选一个布尔值, 表示listener
在添加之后最多只调用一次. 如果为true
,listener
会在其被调用之后自动移除.passive
可选一个布尔值, 设置为true
时, 表示listener
永远不会调用preventDefault()
. 如果 listener 仍然调用了这个函数, 客户端将会忽略它并抛出一个控制台警告. 查看使用 passive 改善滚屏性能以了解更多.signal
可选AbortSignal
, 该AbortSignal
的abort()
方法被调用时, 监听器会被移除.
useCapture
可选一个布尔值, 表示在 DOM 树中注册了
listener
的元素, 是否要先于它下面的EventTarget
调用该listener
. 当 useCapture( 设为 true) 时, 沿着 DOM 树向上冒泡的事件不会触发 listener. 当一个元素嵌套了另一个元素, 并且两个元素都对同一事件注册了一个处理函数时, 所发生的事件冒泡和事件捕获是两种不同的事件传播方式. 事件传播模式决定了元素以哪个顺序接收事件. 进一步的解释可以查看 DOM Level 3 事件及 JavaScript 事件顺序文档. 如果没有指定,useCapture
默认为false
.
addEventListener(type, listener);
addEventListener(type, listener, options);
addEventListener(type, listener, useCapture);
相比于dom.on_xxx((e)=> console.log(e))
这种模式, addEventListener
适用性更好, 例如: 提供的capture
一个布尔值, 表示 listener
会在该类型的事件捕获阶段传播到该 EventTarget
时触发, 可以更好的对事件的传播控制.
document.onclick = (e) => console.log(e);
document.addEventListener('click', (e)=> console.log(e), true);
2.16.2 取消事件监听
signal
可选AbortSignal
, 该AbortSignal
的abort()
方法被调用时, 监听器会被移除.
{
// 在fecth相关的取消也是如此操作
const controller = new AbortController();
const signal = controller.signal;
const options = {
capture: true,
signal: signal
}
document.addEventListener('click', (e)=> console.log(e), options);
setTimeout(() => {
controller.abort(); // 需要chrome93以上版本支持
console.log('cancel');
}, 10000);
}
removeEventListener(type, listener);
removeEventListener(type, listener, options);
removeEventListener(type, listener, useCapture);
element.removeEventListener("mousedown", handleMouseDown, false); // 失败
element.removeEventListener("mousedown", handleMouseDown, true); // 成功
第一个调用失败是因为 useCapture
没有匹配. 第二个调用成功, 是因为 useCapture
匹配相同.
// element.addEventListener("mousedown", handleMouseDown, { passive: true });
element.removeEventListener("mousedown", handleMouseDown, { passive: true }); // 成功
element.removeEventListener("mousedown", handleMouseDown, { capture: false }); // 成功
element.removeEventListener("mousedown", handleMouseDown, { capture: true }); // 失败
element.removeEventListener("mousedown", handleMouseDown, { passive: false }); // 成功
element.removeEventListener("mousedown", handleMouseDown, false); // 成功
element.removeEventListener("mousedown", handleMouseDown, true); // 失败
值得注意的是, 一些浏览器版本在这方面会有些不一致. 除非你有特别的理由, 使用与调用
addEventListener()
时配置的参数去调用removeEventListener()
是明智的.
2.16.3 创建事件
event = new CustomEvent(typeArg, customEventInit);
- typeArg
一个表示 event 名字的字符串
- customEventInit
Is a
CustomEventInit
dictionary, having the following fields: 一个字典类型参数, 有如下字段"detail"
, optional and defaulting tonull
, of type any, that is a event-dependant value associated with the event. 可选的默认值是 null 的任意类型数据, 是一个与 event 相关的值bubbles 一个布尔值, 表示该事件能否冒泡. 来自EventInit
. 注意: 测试 chrome 默认为不冒泡. cancelable 一个布尔值, 表示该事件是否可以取消. 来自EventInit
var event = new Event('build');
// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);
// Dispatch the event.
elem.dispatchEvent(event);
// add an appropriate event listener
obj.addEventListener("cat", function (e) {
process(e.detail);
});
// create and dispatch the event
let event = new CustomEvent("cat", {
detail: {
hazcheeseburger: true,
},
});
obj.dispatchEvent(event);
// Will return an object contaning the hazcheeseburger property
let myDetail = event.detail;
2.16.4 获取元素
两组不同的方式:
文档对象模型
Document
引用的querySelector()
方法返回文档中与指定选择器或选择器组匹配的第一个Element
对象. 如果找不到匹配项, 则返回null
.
- querySelectorAll()
- querySelector()
返回一个包含了所有指定类名的子元素的类数组对象. 当在 document 对象上调用时, 会搜索整个 DOM 文档, 包含根节点. 你也可以在任意元素上调用
getElementsByClassName()
方法, 它将返回的是以当前元素为根节点, 所有指定类名的子元素.
- getElementsByClassName()
- getElementById()
- getElementsByTagName()
两组方法的差异:
- querySelector* is more flexible, as you can pass it any CSS3 selector, not just simple ones for id, tag, or class.
querySelector*使用更灵活, 支持
CSS3 selector
- The performance of querySelector* changes with the size of the DOM that it is invoked on. To be precise, querySelector* calls run in O(n) time and getElement* calls run in O(1) time, where n is the total number of all children of the element or document it is invoked on.
querySelector*的执行效率更低, 时间复杂度为: O(n)
- The return types of these calls vary.
querySelector
andgetElementById
both return a single element.querySelectorAll
andgetElementsByName
both return NodeLists. The oldergetElementsByClassName
andgetElementsByTagName
both return HTMLCollections. NodeLists and HTMLCollections are both referred to as collections of elements.
querySelector
andgetElementById
返回的是单一元素.其余的返回都是
HTMLCollections
集合
- Collections can return "live" or "static" collections respectively. This is NOT reflected in the actual types that they return. getElements* calls return live collections, and querySelectorAll returns a static collection. The way that I understand it, live collections contain references to elements in the DOM, and static collections contain copies of elements. Take a look at @Jan Feldmann's comment's below for a different angle as well. I haven't figured out a good way to incorporate it into my answer but it may be a more accurate understanding.
getElements*返回的是动态集合.
querySelectorAll返回的是静态集合.
Function | Live? | Type | Time Complexity querySelector | | Element | O(n) querySelectorAll | N | NodeList | O(n) getElementById | | Element | O(1) getElementsByClassName | Y | HTMLCollection | O(1) getElementsByTagName | Y | HTMLCollection | O(1) getElementsByName | Y | NodeList | O(1)
# 传入多个元素id
document.querySelector('.history-list .r-info .r-txt')
- querySelectorAll() vs getElementsByClassName ✧ unicorntears.dev ✧
- querySelector and querySelectorAll vs getElementsByClassName and getElementById in JavaScript - Stack Overflow
2.16.5 监听元素改变
MutationObserver
接口提供了监视对 DOM 树所做更改的能力. 它被设计为旧的 Mutation Events 功能的替代品, 该功能是 DOM3 Events 规范的一部分.
mutationObserver.observe(target[, options])
target
DOM 树中的一个要观察变化的 DOM
Node
(可能是一个Element
), 或者是被观察的子节点树的根节点.options
此对象的配置项描述了 DOM 的哪些变化应该报告给
MutationObserver
的callback
. 当调用observe()
时,childList
,attributes
和characterData
中, 必须有一个参数为true
. 否则会抛出TypeError
异常.
options
的属性如下:
subtree
可选当为
true
时, 将会监听以target
为根节点的整个子树. 包括子树中所有节点的属性, 而不仅仅是针对target
. 默认值为false
.
childList
可选当为
true
时, 监听target
节点中发生的节点的新增与删除( 同时, 如果subtree
为true
, 会针对整个子树生效) . 默认值为false
.
attributes
可选当为
true
时观察所有监听的节点属性值的变化. 默认值为true
, 当声明了attributeFilter
或attributeOldValue
, 默认值则为false
.
attributeFilter
可选一个用于声明哪些属性名会被监听的数组. 如果不声明该属性, 所有属性的变化都将触发通知.
attributeOldValue
可选当为
true
时, 记录上一次被监听的节点的属性变化; 可查阅监听属性值了解关于观察属性变化和属性值记录的详情. 默认值为false
.
characterData
可选当为
true
时, 监听声明的target
节点上所有字符的变化. 默认值为true
, 如果声明了characterDataOldValue
, 默认值则为false
characterDataOldValue
可选当为
true
时, 记录前一个被监听的节点中发生的文本变化. 默认值为false
new MutationObserver((event) => {
event.forEach((e) => {
if (e.addedNodes.length === 1) {
let i = e.addedNodes[0];
const c = i.className;
let l = "";
if (
!(
(c &&
typeof c === "string" &&
c.includes("artfullscreen")) ||
((l = i.localName) &&
l &&
typeof l === "string" &&
l.includes("artfullscreen"))
)
) {
i.remove();
i = null;
}
}
});
}).observe(document.body, { childList: true });
2.16.6 页面滚动
window.requestAnimationFrame
window.requestAnimationFrame()
告诉浏览器- - 你希望执行一个动画, 并且要求浏览器在下次重绘之前调用指定的回调函数更新动画. 该方法需要传入一个回调函数作为参数, 该回调函数会在浏览器下一次重绘之前执行.**备注: ** 若你想在浏览器下次重绘之前继续更新下一帧动画, 那么回调函数自身必须再次调用
requestAnimationFrame()
.requestAnimationFrame()
是一次性的.当你准备更新在屏动画时你应该调用此方法. 这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数( 即你的回调函数) . 回调函数执行次数通常是每秒 60 次, 但在大多数遵循 W3C 建议的浏览器中, 回调函数执行次数通常与浏览器屏幕刷新次数相匹配. 为了提高性能和电池寿命, 在大多数浏览器里, 当
requestAnimationFrame()
运行在后台标签页或者隐藏的 iframe 里时,requestAnimationFrame()
会被暂停调用以提升性能和电池寿命.
// 判断页面是否见底
const bottomVisible = () => document.documentElement.clientHeight + window.scrollY >= document.documentElement.scrollHeight || document.documentElement.clientHeight;
class autoscroll {
constructor() {
let stepTime = 40;
let keyic = 1;
let bottom = 100;
const scroll = {};
const stopScroll = () => {
scroll.state = false;
scroll.time = null;
scroll.pos = null;
};
stopScroll(); //初始化
/*
下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数). 该回调函数会被传入DOMHighResTimeStamp参数,
该参数与performance.now()的返回值相同, 它表示requestAnimationFrame() 开始去执行回调函数的时刻.
*/
const pageScroll = (time) => {
const position =
edom.scrollTop || body.scrollTop || window.pageYOffset;
if (scroll.time) {
scroll.pos =
scroll.pos !== null
? scroll.pos + (time - scroll.time) / stepTime
: position;
window.scrollTo(0, scroll.pos);
}
scroll.time = time;
if (scroll.state) {
let tmp = edom.scrollHeight || body.scrollHeight;
tmp = tmp - window.innerHeight - bottom;
if (position < tmp) {
window.requestAnimationFrame(pageScroll);
} else {
stopScroll();
}
}
};
function timespeed(e) {
if (e === true) {
stepTime += 5;
stepTime > 100 ? (stepTime = 100) : stepTime;
} else {
stepTime -= 5;
stepTime < 5 ? (stepTime = 5) : stepTime;
}
}
function start() {
keyic++;
if (keyic % 2 === 1) {
if (scroll.state) {
stopScroll();
} else {
scroll.state = true;
window.requestAnimationFrame(pageScroll);
}
if (hupu.isHupu) hupu.sidebarClear();
}
}
function getstate() {
return scroll.state;
}
function stop() {
if (scroll.state) stopScroll();
}
this.timespeed = timespeed;
this.start = start;
this.getstate = getstate;
this.stop = stop;
}
}
2.17 文本转语音
网页语音 API 的**
SpeechSynthesis
** 接口是语音服务的控制接口; 它可以用于获取设备上关于可用的合成声音的信息, 开始, 暂停语音, 或除此之外的其他命令.
依赖于本地的系统提供的API接口.
class TTS {
constructor() {
//property
const vosp = voiceOptions();
let iscn = false;
let isen = false;
vosp.then((voices) => {
let i = voices.length;
for (i; i--;) {
if (voices[i].localService) {
let tmp = voices[i].lang;
if (tmp.includes("zh")) iscn = true;
if (tmp.includes("en")) isen = true;
if (iscn && isen) break;
}
}
});
let rate = GM_getValue("rate");
rate == null ? (rate = 1.5) : rate;
let syu = new SpeechSynthesisUtterance();
let isplay = false;
let isend = false;
let ispause = false;
//inner_action
function setcontent() {
let content = getselection();
if (content.length === 0) return 1;
let lang = "";
/[\u4e00-\u9fa5]/.test(content)
? (lang = "zh-CN")
: (lang = "en-US");
if (lang.includes("zh") && !iscn) return 2;
syu.rate = rate;
syu.lang = lang;
syu.text = content;
return 0;
}
function pause() {
if (isplay) speechSynthesis.pause();
}
//outer_action
function speed(e) {
if (e) {
rate += 0.2;
if (rate > 4) {
notification("速度已经最大", "警告");
rate = 4;
}
} else {
rate -= 0.2;
if (rate < 0.6) {
notification("速度已经最小", "提示");
rate = 0.6;
return;
}
}
rate = parseFloat(rate.toFixed(1));
notification(
"速度为: " + rate + ", 重新播放后生效",
"播放速度",
1500
);
GM_setValue("rate", rate);
}
function end(e) {
speechSynthesis.cancel();
if (e) syu = null;
}
function play() {
if (isplay) {
pause();
} else {
const i = setcontent();
if (i === 2) {
notification("浏览器没有中文语言包", "警告");
return;
} else if (i === 1) {
notification("未获取到有效内容", "提示");
return;
}
speechSynthesis.speak(syu);
}
}
function resume() {
if (isend) {
speechSynthesis.speak(syu);
} else if (ispause) {
window.speechSynthesis.resume();
}
}
this.end = end;
this.resume = resume;
this.play = play;
this.speed = speed;
//event
syu.onend = () => {
isplay = false;
ispause = false;
isend = true;
notification("播放结束", "提示");
};
syu.onstart = () => {
isplay = true;
isend = false;
ispause = false;
notification("TTS开始播放", "提示", 1500);
};
syu.onpause = () => {
isplay = false;
ispause = true;
notification("播放已暂停", "提示", 1500);
};
syu.onresume = () => {
isplay = true;
isend = false;
ispause = false;
notification("播放已重新开始", "提示", 1500);
};
}
}
三. 箭头函数
箭头函数表达式的语法比函数表达式更简洁, 并且没有自己的
this
,arguments
,super
或new.target
. 箭头函数表达式更适用于那些本来需要匿名函数的地方, 并且它不能用作构造函数.
箭头函数, 不仅让代码变得更简约, 处理this
的指向, 箭头函数的适用性比传统的function
更为便利.
{
// 自定义一个加法函数, 使用箭头函数
const my_sum = (...args) => args.reduce((s, e) => s += e);
console.log(my_sum(1,1,2));
// 传统function
function my_sum_f(...args){
let s = 0;
for (const e of args) s += e;
return s;
}
console.log(my_sum_f(1,1,2));
}
VM369:3 4
VM369:3 4
让很多看似复杂的代码, 只需要一行即可完成, 但是需要注意使用箭头函数, this
的指向.
this
的指向, 在箭头函数下, 和function()
声明的函数的差异.
// 不加大括号
document.onclick = function(e){console.log(this)}; // document
document.onclick = (e) => console.log(this); // window
// 加大括号
{document.onclick = function(e){console.log(this)};} // document
{document.onclick = (e) => console.log(this);} // document
> {
... let group = {
... title: "Our Group",
... students: ["John", "Pete", "Alice"],
...
... showList() {
... this.students.forEach(function(student) {
... // Error: Cannot read property 'title' of undefined
... console.log(this.title + ': ' + student);
... });
... }
... };
...
... group.showList();
... }
undefined: John
undefined: Pete
undefined: Alice
> {
... let group = {
... title: "Our Group",
... students: ["John", "Pete", "Alice"],
...
... showList() {
... this.students.forEach(
... student => console.log(this.title + ': ' + student)
... );
... }
... };
...
... group.showList();
... }
Our Group: John
Our Group: Pete
Our Group: Alice
箭头函数小结:
- 没有
this
(从外部获取) - 没有
arguments
- 不能使用
new
进行调用 - 没有
super
.
交叉对比, python中实现上述的自定义求和函数.
from functools import reduce
# 自定义和上述一致的函数
# 使用2个lambda函数, 和上述的 使用2 * => 类似
my_sum = lambda args: reduce(lambda x, y: x + y, args)
my_sum([1,1,3])
# 5
python
中实现上述的方式, 但是reduce
不再直接可用, 而被移入functools
, 使用颇为不便.
四. this
this, super, prototype
, 这三者的绕圈圈游戏, 是相对难以理解的三个部分.
之前我们说过this 是在运行时进行绑定的, 并不是在编写时绑定, 它的上下文取决于函数调用时的各种条件. this 的绑定和函数声明的位置没有任何关系, 只取决于函数的调用方式. 当一个函数被调用时, 会创建一个活动记录( 有时候也称为执行上下文) . 这个记录会包含函数在哪里被调用( 调用栈) , 函数的调用方法, 传入的参数等信息. this 就是记录的其中一个属性, 会在函数执行的过程中用到.
当前执行上下文( global, function 或 eval) 的一个属性, 在非严格模式下, 总是指向一个对象, 在严格模式下可以是任意值.
一般而言, 判断this
指向的方法:
- 由
new
调用, 绑定到新创建的对象. - 由
call
或者apply
( 或者bind
) 调用, 绑定到指定的对象. - 由上下文对象调用, 绑定到那个上下文对象.
- 默认: 在严格模式下绑定到
undefined
, 否则绑定到全局对象.
4.1 [箭头函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.1 箭头函数)
// 普通函数
> function f1(){
... return this;
... }
// nodejs中的全局对象, 类似于浏览器中的window
> f1() === globalThis;
true
> {
... let obj = {
... bar: function () {
... const x = (() => this);
... return x;
... }
... };
... console.log(obj.bar()() === obj);
... }
true
在箭头函数中, this
与封闭(这是因为箭头函数的this取自外部)词法环境的this
保持一致. 在全局代码中, 它将被设置为全局对象.
{
const test = {
name: 'test',
f1(){
console.log(this.name)
},
f2: () => {
// f2的这个箭头函数没有获取到其关联的外部对象的this, this的指向window这个全局对象上
console.log(this === window);
},
f3: function(){
console.log(this.name)
}
};
for (let i = 1; i < 4; i++) test[`f${i}`]();
}
VM52:6 test
VM52:9 true
VM52:12 test
> {
... const a = {
... id: 1,
// 对"m"这个键, 赋值一个箭头函数
... m: (...args) => { console.log(this.id + "|" + args.join(',')) }
... }
... const b = {
... id: 2
... }
// 哪怕使用了call, bind b对象, 但是上述a中的m, this的指向没有指向b
... a.m.call(b, 4, 5, 6);
... }
undefined|4,5,6
> {
... const a = {
... id: 1,
... m(...args) {console.log(this.id + "|" + args.join(','))}
... }
... const b = {
... id: 2
... }
... a.m.call(b, 4, 5, 6);
... }
2|4,5,6
> {
... const a = {
... id: 1,
// m, 赋值一个普通function函数
... m: function(...args) {console.log(this.id + "|" + args.join(','))}
... }
... const b = {
... id: 2
... }
... a.m.call(b, 4, 5, 6);
... }
2|4,5,6
> {
... const a = {
... id: 1,
//
... m(...args) {console.log(this.id + "|" + args.join(','))}
... }
... const b = {
... id: 2
... }
// 直接调用
... a.m(4, 5, 6);
... }
1|4,5,6
> {
... const a = {
... id: 1,
// 箭头函数
... m: (...args) => {console.log(this.id + "|" + args.join(','))}
... }
... const b = {
... id: 2
... }
... a.m(4, 5, 6);
... }
undefined|4,5,6
> {
... let user = {
... firstName: "John",
... sayHi() {
... console.log(`Hello, ${this.firstName}!`);
... }
... };
... // 这里的操作导致上下文的丢失, this不再指向 user 这个object
// 变更为window(浏览器运行环境)
... setTimeout(user.sayHi, 100);
... }
> Hello, undefined!
> {
... let user = {
... firstName: "John",
... sayHi: () => {
... console.log(`Hello, ${this.firstName}!`);
... }
... };
...
... setTimeout(user.sayHi, 100);
... }
> Hello, undefined!
修复setTimeout()
导致的问题
> {
... let user = {
... firstName: "John",
... sayHi() {
... console.log(this.firstName);
... }
... };
... // 包装多一层的 () => 箭头函数
... setTimeout(()=>user.sayHi(), 100);
... }
> John
> {
... let user = {
... firstName: "John",
... sayHi() {
... console.log(this.firstName);
... }
... };
... // bind(user), 绑定user => this
... setTimeout(user.sayHi.bind(user), 100);
... }
> John
更多相关内容见上面的箭头函数上的信息.
4.2 [对象的方法](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.2 作为对象的方法)
当一个函数用作构造函数时( 使用new关键字) , 它的this
被绑定到正在构造的新对象.
4.3 原型链中
对于在对象原型链上某处定义的方法, 同样的概念也适用. 如果该方法存在于一个对象的原型链上, 那么 this
指向的是调用这个方法的对象, 就像该方法就在这个对象上一样.
4.4 [构造函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.4 作为构造函数)
当一个函数用作构造函数时( 使用new关键字) , 它的this
被绑定到正在构造的新对象.
4.5 [内联事件处理函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.5 作为一个内联事件处理函数)
当代码被内联 on-event 处理函数 (en-US) 调用时, 它的this
指向监听器所在的 DOM 元素.
4.6 [DOM 事件处理函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this### 4.6 作为一个_dom_事件处理函数)
当函数被用作事件处理函数时, 它的 this
指向触发事件的元素( 一些浏览器在使用非 addEventListener
的函数动态地添加监听函数时不遵守这个约定) .
<button onclick="this.style.display='none'">点我后我就消失了</button>
五. 函数
let func = new Function ([arg1, arg2, ...argN], functionBody);
> {
... const my_sum = new Function('a', 'b', 'return a + b');
... console.log(my_sum(1, 2));
... }
3
var x = 10;
function createFunction1() {
var x = 20;
return new Function('return x;'); // 这里的 x 指向最上面全局作用域内的 x
}
function createFunction2() {
var x = 20;
function f() {
return x; // 这里的 x 指向上方本地作用域内的 x
}
return f;
}
var f1 = createFunction1();
console.log(f1()); // 10
var f2 = createFunction2();
console.log(f2()); // 20
5.1 构造函数
Function() 构造函数创建了一个新的 Function 对象. 直接调用构造函数可以动态创建函数, 但可能会经受一些安全和类似于 eval()( 但远不重要) 的性能问题. 然而, 不像 eval( 可能访问到本地作用域) , Function 构造函数只创建全局执行的函数.
> {
... const sum = new Function('a', 'b', 'return a + b');
... console.log(sum(2, 6));
... }
8
在vscode
中进行如此的书写代码, 会自动提示要不要转为更合理严谨的class
模式.
> {
... function parent(){
... this.family ='abc';
... }
... function son(name){
// 还是优先使用class为好
... parent.apply(this, arguments);
... this.name = name;
... }
... const s = new son('jam');
... console.log(s.name);
... console.log(s.family);
... }
jam
abc
在构造函数实现类中的继承特性.
5.2 call/bind/apply
> {
... const a = {
... id: 1,
... m(...args) { console.log(this.id + "|" + args.join(',')) }
... }
... const b = {
... id: 2
... }
... a.m.call(b, 4, 5, 6);
... a.m.call(b, 4, 5, 6, [7, 8]);
... a.m.apply(b, [4,5,6]);
... a.m.bind(b, [4, 5, 6])();
... a.m.bind(b, [4, 5, 6], 7)();
... a.m.bind(b, [4, 5, 6]).bind(a, 0)();
... }
2|4,5,6
2|4,5,6,7,8
2|4,5,6
2|4,5,6
2|4,5,6,7
2|4,5,6,0
> {
... var id = 3;
... const a = {
... id: 1,
... m(...args) { console.log(this.id + "|" + args.join(',')) }
... }
... const b = {
... id: 2
... }
... a.m.call(null, 4, 5, 6);
... }
3|4,5,6
三者的差异:
- 以上三者均可改变目标的this指向, 如果如果没有这个参数或参数为
undefined
或null
, 则默认指向全局window
. call, apply
, 立即执行.apply
, 参数只支持数组结构.bind, call
, 支持参数列表.bind
支持多次参数传递.
手动实现自定义的bind
函数:
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
const args = [...arguments].slice(1), fn = this;
return function Fn() {
// 根据调用方式, 传入不同绑定值
return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments));
}
}
来看看简书的一段代码使用apply
的场景, 这些代码乍看之下貌似很复杂的样子.
const colorful_Console = {
colors: {
warning: "#F73E3E",
Tips: "#327662",
info: "#1475b2",
},
main(info, bc) {
// 这段代码引用自简书, 做了轻微的修改
// 目的是打印彩色的log
// console.log('%c123%c456','color: red','color: green')
const t = info.title,
c = info.content,
a = [
"%c ".concat(t, " %c ").concat(c, " "),
"padding: 1px; border-radius: 3px 0 0 3px; color: #fff; font-size: 12px; background: ".concat(
"#606060",
";"
),
"padding: 1px; border-radius: 0 3px 3px 0; color: #fff; font-size: 12px; background: ".concat(
bc,
";"
),
];
// 也可以变成这样子
// console.log.apply(null, ['%c123%c456','color: red','color: green']);
// 实际上, console.log(...a); 即等价于上述一堆乱七八糟的代码
// 这部分的代码为了兼容?还是出于其他的目的?
(function () {
let e;
window.console &&
"function" === typeof window.console.log &&
(e = console).log.apply(e, arguments);
}.apply(null, a), a); // 去掉最后的a? 有什么影响
},
};
colorful_Console.main({'title': 'abc', 'content': 'content'}, colorful_Console.colors.warning)
// 压缩为: 简洁明了版本
const colorful_Console = {
colors: {
warning: "#F73E3E",
Tips: "#327662",
info: "#1475b2",
},
main(info, bc) {
const a = [
`%c ${info.title} %c ${info.content} `,
"padding: 1px; border-radius: 3px 0 0 3px; color: #fff; font-size: 12px; background: #606060;",
`padding: 1px; border-radius: 0 3px 3px 0; color: #fff; font-size: 12px; background: ${bc};`
];
console.log(...a);
},
};
colorful_Console.main({ 'title': 'abc', 'content': 'content' }, colorful_Console.colors.warning);
5.3 偏函数
偏函数, 或者是函数柯里化, 即使用部分参数的函数, 在python
中实现偏函数的方式:
# 初始函数
init_func = lambda a: lambda b, c: a + b + c
# 偏函数
p_func = init_func(1) # 先使用1个参数: 1
p_func(2, 3)
# 6 = 1 + 2 + 3
p_func(3, 4)
# 8 = 1+ 3 + 4
# 或者使用partial函数实现
from functools import partial
def test(a, b, c):
return a + b + c
test_c = partial(test, a=1)
print(test_c(b=2, c=3))
在JavaScript
中实现, 可以通过bind
> {
... const init_func = function(a, b, c) { return a + b + c };
// 等价 const init_func = (a, b, c) => a + b + c ;
... const p_func = init_func.bind(null, 1);
... console.log(p_func(2, 3));
... console.log(p_func(3, 4));
... }
6
8
5.4 以变量为函数名
{
let a = 'say';
let b = 'hi';
class User {
[a + b]() {
console.log("Hello");
}
}
new User()[a + b]();
}
// Hello
这对于指向某些代码变得异常方便, 可以使用循环批量执行代码.
> {
... const f_ms = {
... f1() {console.log('f1')},
... f2() {console.log('f2')},
... f3() {console.log('f3')},
... };
... for (let i = 1; i<4;i++) f_ms[`f${i}`]();
... }
f1
f2
f3
5.5 自执行
> {
... (()=> console.log('hello'))();
... // (()=> console.log('hello')) = 无名函数func > func()
... (function(){console.log('world')})();
... }
hello
world
// 带有参数
> ((name)=> console.log('hello ' + name))('alex');
hello alex
六. 异步
JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是, 它永不阻塞. 处理 I/O 通常通过事件和回调来执行, 所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时, 它仍然可以处理其他事情, 比如用户输入.
由于历史原因有一些例外, 如
alert
或者同步 XHR, 但应该尽量避免使用它们. 注意, 例外的例外也是存在的( 但通常是实现错误而非其他原因) .
异步, 举个简单的例子 单一线程内的程序A, 向服务器1发出请求, 需要等到20ms时间返回内容, 在这等待期间继续向服务器2发出请求, 需要等待30ms....如此, 单一线程的程序, 可以同时(几乎)发起大量请求(高并发), 而不是发出一个请求, 然后空等数据的返回, 异步的一大好处在于执行效率非常高(例如python中, 异步的执行效率在IO密集型的任务中(如爬虫), 通常比多线程, 多进程高很多) .
多线程也是编程世界中非常重要的角色, 很多语言具备多线程能力. 但是多线程涉及线程安全问题, 为了保证js的安全性, js语言被设计为单线程应用. 浏览器本身也是多线程, 包括
- javascript引擎线程
- 界面渲染线程
- 浏览器事件触发线程
- Http请求线程
这些线程在js运行的时候同时运行, 不同的任务会被分配到不同的线程中, 再通过异步的方式相互通知, js编程只能在js引擎线程中工作, 因此说js是单线程的. 但是现代HTML标准使得在浏览器环境中( node也可以) js具备了多线程编程能力, 即通过webworker实现. 在主线程和worker线程之间, 也是通过异步方式相会调用( 通知) . 多线程可以解放单线程带来的一些运算能力局限, 例如, 在你的代码中, 有一段代码需要对100K行的数据进行处理, 每次程序运行到这里, 你的浏览器就开始卡, 即使你把它放在异步任务中进行处理, 也会一样. 但是, 如果你把这段代码放到另外一个worker线程中处理, 处理完之后, 把结果返回给主线程, 那效果就不一样, worker线程中的代码不会对界面渲染产生任何影响( 当然, 当内存用完的情况除外) , 因此, 只要使用合理, 可以对web应用起到性能提升的作用.
不要搞混, js是单线程, 但是不意味着浏览器是单线程.
js是所有编程语言里最容易实现异步操作的语言.
应该说JavaScript
和异步如影随形的, 其实现非常容易, 相比于python中的异步, JavaScript
中的异步基本和普通的同步代码没什么区别, 感受不到其特别之处的存在, 开盖即用.
import time
import asyncio # 异步框架
async def a(delay, words):
print(f"before {words}")
await asyncio.sleep(delay)
print(f"after {words}")
async def c():
print(f"started: {time.strftime('%X')}")
# 任务按顺序执行, 等待a1, 完成, 然后执行a2, 即等待的时间未 +1, + 2 = 3
await a(1, "task 1")
# 两个任务是分开等待的, 所以最终的完成运行的时间间隔未3秒
await a(2, "task 2")
print(f"finished: {time.strftime('%X')}")
async def b():
print(f"start: {time.strftime('%X')}")
task1 = asyncio.create_task(a(1, "task 1"))
task2 = asyncio.create_task(a(2, "task 2"))
# 先将这两个任务首先执行了, 然后等待(这个等待是同时发生)
await task1
# + 1
await task2
# 这里由于是同时等待, 所以已经消耗掉1s, 所以最后只需要等待1s
# + 1
# 最终完成运行的时间间隔的时间是2秒
print(f"finish: {time.strftime('%X')}")
async def e():
print(f"start: {time.strftime('%X')}")
# 将所有的任务集中起来执行
tasks = (asyncio.create_task(a(i, f"task {i}")) for i in range(1, 3))
await asyncio.gather(*tasks)
# 等价于
# 也等价于b的执行方式
# await asyncio.gather(a(1, "task 1"), a(2, "task 2"))
print(f"finish: {time.strftime('%X')}")
asyncio.run(b())
# asyncio.run(c())
{
function test(){
console.log('先执行');
setTimeout(() => {
console.log('最后轮到我执行')
}, 100);
console.log('先去执行其他');
}
test();
console.log('我第二');
}
VM26:3 先执行
VM26:7 先去执行其他
VM26:10 我第二
VM26:5 最后轮到我执行
6.1 Promise
需要注意的是Promise
的四种等待结果返回的方式的差异, api的命名很好区分.
func | 作用 |
---|---|
Promise.all() | 所有的Promise返回resolve的结果, 假如返回有任意rejected, 立刻退出, 不等待其他的执行完. |
Promise.allSettled() | 所有的结果全部返回, 结果集中在一起, catch不到reject, 需要在结果中做区分. |
Promise.any() | 获得任意一个resolve则退出, 不等待其他的结果. |
Promise.race() | 获得一个最快返回的内容, 不区分resolve, reject, 获得即退出, 不等待其他结果 |
Promise
返回结果的三种状态:
- pending : 进行中, 表示 Promise 还在执行阶段, 没有执行完成.
- fulfilled: 成功状态, 表示 Promise 成功执行完成.
- rejected: 拒绝状态, 表示 Promise 执行被拒绝, 也就是失败.
6.1.1 all
{
console.time();
const p1 = new Promise((resolve) => setTimeout(() => resolve('ok'), 100));
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('fail'), 200));
const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
await Promise.all([p1, p2, p3]).then((val) => console.log(val)).catch((error) => console.log(error));
console.timeEnd();
}
VM382:6 fail
VM382:7 default: 209.258056640625 ms
6.1.2 race
{
console.time();
const p1 = new Promise((resolve, reject) => setTimeout(() => reject('fail1'), 100));
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('fail'), 200));
const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
await Promise.race([p1, p2, p3]).then((val) => console.log(val)).catch((error) => console.log(error));
console.timeEnd();
}
VM386:6 fail1
VM386:7 default: 113.2451171875 ms
6.1.3 allsettled
{
console.time();
const p1 = new Promise((resolve, reject) => setTimeout(() => reject('fail1'), 100));
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('fail'), 200));
const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
// 注意这里的结果返回
await Promise.allSettled([p1, p2, p3]).then((val) => console.log(val)).catch((error) => console.log(error));
console.timeEnd();
}
VM390:6 (3) [{…}, {…}, {…}]0: {status: 'rejected', reason: 'fail1'}1: {status: 'rejected', reason: 'fail'}2: {status: 'fulfilled', value: 'ok'}length: 3[[Prototype]]: Array(0)
VM390:7 default: 300.99609375 ms
{
console.time();
const p1 = new Promise((resolve, reject) => setTimeout(() => reject('fail1'), 100));
const p2 = new Promise((resolve, reject) => setTimeout(() => {
resolve('ok');
}, 200));
const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
await Promise.allSettled([p1, p2, p3]).then(
(vals) => vals.forEach(e => console.log(e.status + "|" + e.reason + "|" + e.value))
);
console.timeEnd();
}
VM456:9 rejected|fail1|undefined
2VM456:9 fulfilled|undefined|ok
default: 311.851806640625 ms
6.1.4 any
{
console.time();
const p1 = new Promise((resolve, reject) => setTimeout(() => reject('fail1'), 100));
const p2 = new Promise((resolve, reject) => setTimeout(() => resolve('ok'), 200));
const p3 = new Promise((resolve) => setTimeout(() => resolve('ok'), 300));
await Promise.any([p1, p2, p3]).then((val) => console.log(val)).catch((error) => console.log(error));
console.timeEnd();
}
VM430:6 ok
VM430:7 default: 200.421142578125 ms
6.2 async/await
async 函数是使用
async
关键字声明的函数. async 函数是AsyncFunction
构造函数的实例, 并且其中允许使用await
关键字.async
和await
关键字让我们可以用一种更简洁的方式写出基于Promise
的异步行为, 而无需刻意地链式调用promise
.
async
, 提供了更好的异步和同步执行的管理.
> async function a() { return 'a' }
> async function b() { return 'b' }
> async function c() { throw 'fail' }
> async function d() { throw 'another fail' }
> const results = await Promise.all([
... a().catch(e => e),
... b().catch(e => e),
... c().catch(e => e),
... d().catch(e => e)
... ]);
> console.log(results);
[ 'a', 'b', 'fail', 'another fail' ]
> a() // 得到的是一个Promise对象
Promise {
'a',
[Symbol(async_id_symbol)]: 184,
[Symbol(trigger_async_id_symbol)]: 5
}
async
实际上是对Promise
的进一步封装.
{
// nodejs不支持, console.time, 非标准api
console.time();
const test = {
async m(){
return new Promise((resolve, reject) => setTimeout(() => {resolve('ok')}, 1000))
}
}
await test.m();
// 非标准api, console.time
console.timeEnd();
}
VM47:9 default: 1010.76416015625 ms
// 不能以这种方式存在
function t(){
// await, async必须是成对的出现
await test.m();
}
t()
// 箭头函数的表示方式
const test = async () => {};
6.3 Generator(生成器)
function* name([param[, param[, ... param]]]) { statements }
需要注意, 生成器函数不能当构造器使用.
function* f() {}
var obj = new f; // throws "TypeError: f is not a constructor"
需要注意的是JavaScript中的生成器中的return
是能够返回结果的, 有别于python
.
def test():
return 1 #
yield 0 # yield只要存在, 得到的必然是生成器
for e in test():
print(e) # 不会有结果
function* yieldAndReturn() {
yield "Y";
return "R"; //显式返回处, 可以观察到 done 也立即变为了 true
yield "unreachable";// 不会被执行了
}
var gen = yieldAndReturn()
console.log(gen.next()); // { value: "Y", done: false }
console.log(gen.next()); // { value: "R", done: true }
console.log(gen.next()); // { value: undefined, done: true }
多个生成器相互调用.
function* anotherGenerator(i) {
yield i + 1;
yield i + 2;
yield i + 3;
}
function* generator(i){
yield i;
yield* anotherGenerator(i);// 移交执行权
yield i + 10;
}
var gen = generator(10);
// 得到第一个迭代器的第一个返回结果
console.log(gen.next().value); // 10
// 第二个迭代器
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12
console.log(gen.next().value); // 13
// 第一个迭代器, 最后的结果
console.log(gen.next().value); // 20
在执行中, 传递参数.
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2
// first =4 是 next(4) 将参数赋给上一条的
yield second + 3; // 5 + 3
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.next(5)); // "{ value: 8, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
使用生成器生成斐波那契数列.
{
function* test(max){
let a= 0, b = 1;
while (max > 0) {
[a, b] = [b, a +b];
max--;
yield a
}
}
// 注意差异
for (const e of test(5)) console.log(e);
}
1
1
2
3
5
> {
... function* test(max){
... let a= 0, b = 1;
... while (max > 0) {
... [a, b] = [b, a +b];
... max--;
... yield a
...
... }
... }
... const t = test(5)
... console.log(t.next());
... console.log(t.next());
... }
{ value: 1, done: false }
{ value: 1, done: false }
> {
... let a = 1, b = 2;
... // a, b = 30, 40; 注意JavaScript中的元素交换不能像python那样, 需要 [] = []
... a,
... b = 30,
... 40;
... console.log(a);
... console.log(b);
... }
1
30
在python
中实现斐波那契数列.
'''
function* fib(max){
let a= 0, b = 1;
while (max > 0) {
[a, b] = [b, a +b]; // JavaScript不支持python的更为直接的元素交换
max--;
yield a
}
'''
# 代码的相近程度
def fib(max):
a, b = 0, 1
while max > 0:
a, b = b, a+b
max -= 1 ## python不支持 -- 运算符
yield a
for e in fib(5):
print(e)
对symbol
章节的迭代器的内容进行修改:
// 实际上两个部分的代码都是此函数
{
function* itera(start, end) {
let val = start;
while (val < end) {
yield val;
val++;
}
}
for (const e of itera(1, 5)) console.log(e);
}
{
const iterable_dic = {
start: 1,
end: 1,
// *[Symbol.iterator]() 等价
[Symbol.iterator]: function* () {
let val = this.start;
while (val < this.end){
yield val;
val++;
}
}
};
iterable_dic.start = 1;
iterable_dic.end = 5;
for (const e of iterable_dic) console.log(e);
}
VM96:16 1
VM96:16 2
VM96:16 3
VM96:16 4
6.4 XMLHttpRequest
AJAX, Asynchronous JavaScript and XML
XMLHttpRequest
( XHR) 对象用于与服务器交互. 通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL, 获取数据. 这允许网页在不影响用户操作的情况下, 更新页面的局部内容.XMLHttpRequest
在 AJAX 编程中被大量使用.尽管名称如此,
XMLHttpRequest
可以用于获取任何类型的数据, 而不仅仅是 XML. 它甚至支持 HTTP 以外的协议( 包括 file:// 和 FTP) , 尽管可能受到更多出于安全等原因的限制.如果您的通信流程需要从服务器端接收事件或消息数据, 请考虑通过
EventSource
接口使用服务器发送事件. 对于全双工的通信, WebSocket 可能是更好的选择.
{
// 简单封装
function httpXMLRequest(
url,
mode = false,
method = "GET",
sendData = null,
type = "json"
) {
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.timeout = 2500;
if (mode) xhr.withCredentials = true; //不能发送cookie, 必须设置此属性, 否则将出错
if (type) xhr.responseType = type;
xhr.ontimeout = function () {
console.error("warning: time out.");
resolve(null);
};
xhr.onload = () => {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200) {
let response = xhr.response;
if (!response) {
console.log("err, get responese fail");
reject(null);
}
//let code = response.code;
//if (code !== 0) resolve(null);
resolve(response);
} else {
console.log("err:" + xhr.status);
reject(null);
}
} else {
console.log("err:" + xhr.readyState);
reject(null);
}
};
xhr.onerror = (e) => {
console.error("warning: " + e);
reject(null);
};
xhr.send(sendData);
});
}
}
6.5 Fetch API
// fetch可以轻易实现链式操作
fetch('https://www.baidu.com/').then(response => response.text()).then(data => console.log(data));
这种功能以前是使用
XMLHttpRequest
实现的. Fetch 提供了一个更理想的替代方案, 可以很容易地被其他技术使用, 例如Service Workers
. Fetch 还提供了专门的逻辑空间来定义其他与 HTTP 相关的概念, 例如 CORS 和 HTTP 的扩展.请注意,
fetch
规范与jQuery.ajax()
主要有以下的不同:
- 当接收到一个代表错误的 HTTP 状态码时, 从
fetch()
返回的 Promise 不会被标记为 reject, 即使响应的 HTTP 状态码是 404 或 500. 相反, 它会将 Promise 状态标记为 resolve( 如果响应的 HTTP 状态码不在 200 - 299 的范围内, 则设置 resolve 返回值的ok
属性为 false) , 仅当网络故障时或请求被阻止时, 才会标记为 reject.fetch
不会发送跨域 cookie, 除非你使用了 credentials 的初始化选项. ( 自 2018 年 8 月以后, 默认的 credentials 政策变更为same-origin
. Firefox 也在 61.0b13 版本中进行了修改)
fetch()
的功能与 XMLHttpRequest 基本相同, 但有三个主要的差异.( 1)
fetch()
使用 Promise, 不使用回调函数, 因此大大简化了写法, 写起来更简洁.( 2)
fetch()
采用模块化设计, API 分散在多个对象上( Response 对象, Request 对象, Headers 对象) , 更合理一些; 相比之下, XMLHttpRequest 的 API 设计并不是很好, 输入, 输出, 状态都在同一个接口管理, 容易写出非常混乱的代码.( 3)
fetch()
通过数据流( Stream 对象) 处理数据, 可以分块读取, 有利于提高网站性能表现, 减少内存占用, 对于请求大文件或者网速慢的场景相当有用. XMLHTTPRequest 对象不支持数据流, 所有的数据必须放在缓存里, 不支持分块读取, 必须等待全部拿到后, 再一次性吐出来.
// 一个简单封装示例, 用于处理Sogou的重定向
{
const anti_redirect = async (node, url, index = 0) => {
const controller = new AbortController();
let timeout_id = setTimeout(() => {
timeout_id = null;
controller.abort();
console.log(`timeout error: ${url}`);
}, this.options.timeout);
await fetch(url, { ...this.options, signal: controller.signal })
.then((res) => {
if (timeout_id)
clearTimeout(timeout_id), (timeout_id = null);
if (res.status === 200) return res.text();
else throw new Error(`error, httpCode: ${res.status}`);
})
.then((res) => {
if (res.slice(0, 50).includes("<!DOCTYPE html>"))
throw new Error("There is no final site for this link");
else {
const ms = res.match(this.configs.content_reg);
ms && ms.length > 1
? this.configs.func(node, ms, index, url)
: console.log(`no found finalURL in ${url}`);
}
})
.catch((e) => {
timeout_id && clearTimeout(timeout_id);
console.log(`some errors in ${url}`);
console.log(e);
});
}
}
fetch
虽然被各路神仙吹得天花乱坠, 但是却还不是很完善, 例如超时(timeout), 这么基本的东西, 都无法做到只要简单设置即可的地步.
七. 元编程
是一种编程技术, 编写出来的计算机程序能够将其他程序作为数据来处理. 意味着可以编写出这样的程序: 它能够读取, 生成, 分析或者转换其它程序, 甚至在运行时修改程序自身.
7.1 Object
在 JavaScript 中, 几乎所有的对象都是
Object
的实例; 一个典型的对象从Object.prototype
继承属性( 包括方法) , 尽管这些属性可能被覆盖( 或者说重写) . 唯一不从Object.prototype
继承的对象是那些null
原型对象, 或者是从其他null
原型对象继承而来的对象.通过原型链, 所有对象都能观察到
Object.prototype
对象的改变, 除非这些改变所涉及的属性和方法沿着原型链被进一步重写. 尽管有潜在的危险, 但这为覆盖或扩展对象的行为提供了一个非常强大的机制. 为了使其更加安全,Object.prototype
是核心 JavaScript 语言中唯一具有不可变原型的对象- -Object.prototype
的原型始终为null
且不可更改.
func | 作用 |
---|---|
Object.assign() | 浅复制, 将所有可枚举的自有属性从一个或多个源对象复制到目标对象, 返回修改后的对象. 如果目标对象与源对象具有相同的 key, 则目标对象中的属性将被源对象中的属性覆盖, 后面的源对象的属性将类似地覆盖前面的源对象的属性. |
Object.create() | 创建一个新对象, 使用现有的对象来作为新创建对象的原型( prototype) . |
Object.entries() | 返回一个给定对象自身可枚举属性的键值对数组, 其排列与使用 for...in 循环遍历该对象时返回的顺序一致( 区别在于 for-in 循环还会枚举原型链中的属性) . |
Object.freeze() | 冻结一个对象. 不能向这个对象添加新的属性, 不能删除已有属性, 不能修改该对象已有属性的可枚举性, 可配置性, 可写性, 以及不能修改已有属性的值, 对象的原型也不能被修改. |
Object.fromEntries() | 将键值对列表转换为一个对象 |
Object.getOwnPropertyDescriptor() | 获取对象上一个自有属性对应的属性描述符. |
Object.getOwnPropertyDescriptors() | 获取一个对象的所有自身属性的描述符. ( 自有属性指的是直接赋予该对象的属性, 不需要从原型链上进行查找的属性) |
Object.getOwnPropertyNames() | 获取指定对象的所有自身属性的属性名( 包括不可枚举属性但不包括 Symbol 值作为名称的属性) 组成的数组. |
Object.getOwnPropertySymbols() | 获取指定对象自身的所有 Symbol 属性的数组. |
Object.getPrototypeOf() | 返回指定对象的原型( 内部[[Prototype]]属性的值) . |
Object.hasOwn() | 如果指定的对象自身有指定的属性, 返回 true. 如果属性是继承的或者不存在, 返回 false. |
Object.prototype.hasOwnProperty() | 返回一个布尔值, 表示对象自身属性中是否具有指定的属性. |
Object.is() | 判断两个值是否为同一个值 |
Object.isExtensible() | 判断一个对象是否是可扩展的( 是否可以在它上面添加新的属性) . 注意冻结对象也是一个不可扩展的对象. 一个不可扩展的空对象同时也是一个冻结对象. |
Object.isFrozen() | 判断一个对象是否被冻结 |
Object.prototype.isPrototypeOf() | 返回一个布尔值, 表示一个对象是否存在于另一个对象的原型链上. |
Object.isSealed() | 判断一个对象是否被密封. 注意冻结对象也是一个密封对象. |
Object.keys() | 返回一个由一个给定对象的自身可枚举属性组成的数组, 数组中属性名的排列顺序与使用for...in 循环的顺序相同. |
Object.preventExtensions() | 让一个对象变的不可扩展, 也就是永远不能再添加新的属性. |
Object.seal() | 密封一个对象, 阻止添加新属性并将所有现有属性标记为不可配置. 当前属性的值只要原来是可写的就可以改变. |
Object.setPrototypeOf() | 设置一个指定的对象的原型( 即, 内部 [[Prototype]] 属性) 到另一个对象或null |
Object.prototype.toString() | 返回一个表示该对象的字符串. |
Object.prototype.valueOf() | 将 this 值转换为一个对象. 此方法旨在用于自定义类型转换的逻辑时, 重写派生类对象. |
Object.values() | 返回一个给定对象自身的所有可枚举属性值的数组, 值的顺序与使用for...in 循环的顺序相同. |
功能很多, 较为常用的如下:
7.1.1 浅拷贝
> {
... const a = {'b': 1, 'a': [1,1,2]};
... const b = Object.assign({}, a);
... a.a = 10; // 修改
... console.log(b); //b没有改变
... }
{ b: 1, a: [ 1, 1, 2 ] }
> {
... const a = {'b': 1, 'a': [1,1,2]};
... const b = Object.assign({}, a);
... a.b = 10;
... console.log(b);
... }
{ b: 1, a: [ 1, 1, 2 ] }
> {
... const a = {'b': 1, 'a': [1,1,2]};
... const b = Object.assign({}, a);
... a.a[0] = 10;
... console.log(b); // b改变
... }
{ b: 1, a: [ 10, 1, 2 ] }
> {
... const a = {'a': 1, 'b': 2};
... const b = Object.assign({}, a);
... console.log(Object.getOwnPropertyDescriptor(b, 'b'));
... }
{ value: 2, writable: true, enumerable: true, configurable: true }
7.1.2 属性描述符
> {
... const a = {'a': 1, 'b': 2};
// 空值a.a的属性的读取和操作
... Object.defineProperty(a, 'a', {
... value: 20,
... writable: false, // 默认这几项全部为true
... configurable: false,
... enumerable: false
... });
... a.a = 40; // 赋值操作无效
... console.log(a.a);
... for (const e in a) console.log(e); // 枚举无效
... const b = Object.assign({}, a); // 无法以枚举的方式分配(enumerable)到新的对象
... console.log(b.a);
... }
20
b
undefined
get
用作属性 getter 的函数, 如果没有 getter 则为
undefined
. 当访问该属性时, 将不带参地调用此函数, 并将this
设置为通过该属性访问的对象( 因为可能存在继承关系, 这可能不是定义该属性的对象) . 返回值将被用作该属性的值. **默认值为undefined
. **
set
用作属性 setter 的函数, 如果没有 setter 则为
undefined
. 当该属性被赋值时, 将调用此函数, 并带有一个参数( 要赋给该属性的值) , 并将this
设置为通过该属性分配的对象. **默认值为undefined
. **如果描述符没有
value
,writable
,get
和set
键中的任何一个, 它将被视为数据描述符. 如果描述符同时具有 [value
或writable
] 和 [get
或set
] 键, 则会抛出异常.
{
const a = { 'a': 1, 'b': 2 };
const cach = [];
Object.defineProperty(a, 'a', {
configurable: true,
enumerable: true,
get(){
console.log('get');
return 'nothing';
},
set(val){
// 注意这里的拦截操作, 不能像Proxy那样子, 将属性设置到 a 上, 会无限调用, 导致堆栈溢出
// if(val > 1) Reflect.set(this, 'a', val);
if (cach.includes(val)) return;
cach.push(val);
}
});
a.a = 10;
console.log(a.a);
}
VM35:8 get
VM35:17 nothing
设置钩子拦截某些操作, 通常根据传递的参数来执行某些操作, 而这些操作是在target obejct
上进行的, Object.defineProperty
就没有Proxy
这么强大易用了.
{
let a = { 'a': 1, 'b': 2 };
a = new Proxy(a, {
set(...args) {
// this, 'a', 10, proxy
// 加多一个判断, args[1] === 'a'
const val = args[2];
if (val > 1) {
console.log(`nice val ${val}`);
Reflect.set(...args);
}
else console.log(`to small: ${val}`);
}
});
a.a = 10;
console.log(a.a);
}
nice val 10
10
class X {
constructor() {
// Create a non-writable property
Object.defineProperty(this, 'prop', {
configurable: true,
writable: false,
value: 1,
});
}
}
class Y extends X {
constructor() {
super();
}
foo() {
super.prop = 2; // Cannot overwrite the value.
}
}
const y = new Y();
y.foo(); // TypeError: "prop" is read-only
console.log(y.prop); // 1
和class
搭配使用.
7.1.2.1 私有属性问题
但是需要注意的是, 对于类的隐藏(私有)属性捕捉问题.
{
class test {
#abc = null;
abc = null;
constructor(){
Object.defineProperty(this, 'abc', {
set(v) {
console.log(v)
}
});
Object.defineProperty(this, '#abc', {
set(v) {
console.log(v)
}
});
}
a(){
this.#abc = 10;
this.abc = 100;
}
}
new test().a();
}
100
私有属性是拦截不到的.
{
class test {
#abc = null;
abc = null;
constructor(){
}
a(){
this.#abc = 10;
this.abc = 100;
}
b(){
console.log(Object.getOwnPropertyDescriptors(this));
}
}
const a = new test();
a.b();
console.log(Object.getOwnPropertyDescriptors(a));
console.log(Object.getOwnPropertyNames(a));
//
console.log(Object.getOwnPropertySymbols(a));
}
只能读取到abc
, 无法读取到#abc
.
Object.getOwnPropertySymbols()
静态方法返回一个包含给定对象所有自有 Symbol 属性的数组.
这里也无法读取到私有属性.
7.2 Reflect/Proxy
Proxy & Reflect
通常是成对存在的, 以下面的数组为例子:
Reflect 对象上面的方法和 Proxy 上面都是一一对应的, 参数也是完全相同的, 因此在 Proxy 中可以使用 Reflect 来操作对象.
{
// 拦截数组的push操作, 只允许特定的数据执行push
window.Array.prototype.push = new Proxy(window.Array.prototype.push, {
apply(...args){
console.log(args);
const p = args[args.length - 1][0]; // push需要添加的值
if (p > 1) {
console.log('push')
Reflect.apply(...args); // 执行push的操作, 但是不会触发Proxy拦截器
}else console.log('the number is too small')
},
});
const a = [1];
a.push(0);
console.log(a);
const b = new Array(2,3);
b.push(4);
console.log(b);
}
VM827:5 (3) [ƒ, Array(1), Array(1)]
VM827:10 the number is too small
VM827:15 [1]
VM827:5 (3) [ƒ, Array(2), Array(1)]
VM827:8 push
VM827:19 (3) [2, 3, 4]
简单理解, Proxy
负责拦截, Reflect
负责恢复Proxy
拦截的target function
, 这是为了避免无限递归操作, 这一点是Object.defineProperty
所无法做到的.
class Test:
def __get__(self, instance, owner):
print('get')
def __getattribute__(self, item):
# 拦截属性的读取
# 假如直接执行或者
# return self.__dict__[item], 将导致无限递归, RecursionError: maximum recursion depth exceeded
return super().__getattribute__(item)
def __init__(self) -> None:
self.name = 'abc'
def m(self):
print(self.name)
t = Test()
t.m()
7.2.1 Proxy
const proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
对象中的方法 | 对应触发条件 |
---|---|
handler.getPrototypeOf() | Object.getPrototypeOf 方法的捕捉器 |
handler.setPrototypeOf() | Object.setPrototypeOf 方法的捕捉器 |
handler.isExtensible() | Object.isExtensible 方法的捕捉器 |
handler.preventExtensions() | Object.preventExtensions 方法的捕捉器 |
handler.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor 方法的捕捉器. |
handler.defineProperty() | Object.defineProperty 方法的捕捉器 |
handler.has() | in 操作符的捕捉器 |
handler.get() | 属性读取操作的捕捉器 |
handler.set() | 属性设置操作的捕捉器 |
handler.deleteProperty() | delete 操作符的捕捉器 |
handler.ownKeys() | Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器 |
handler.apply() | 函数被apply调用操作的捕捉器 |
handler.construct() | new 操作符的捕捉器 |
7.2.2 Reflect
Reflect
对象与Proxy
对象一样, 也是 ES6 为了操作对象而提供的新 API.Reflect
对象的设计目的有这样几个.( 1) 将
Object
对象的一些明显属于语言内部的方法( 比如Object.defineProperty
) , 放到Reflect
对象上. 现阶段, 某些方法同时在Object
和Reflect
对象上部署, 未来的新方法将只部署在Reflect
对象上. 也就是说, 从Reflect
对象上可以拿到语言内部的方法.( 2) 修改某些
Object
方法的返回结果, 让其变得更合理. 比如,Object.defineProperty(obj, name, desc)
在无法定义属性时, 会抛出一个错误, 而Reflect.defineProperty(obj, name, desc)
则会返回false
.( 3) 让
Object
操作都变成函数行为. 某些Object
操作是命令式, 比如name in obj
和delete obj[name]
, 而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为.( 4)
Reflect
对象的方法与Proxy
对象的方法一一对应, 只要是Proxy
对象的方法, 就能在Reflect
对象上找到对应的方法. 这就让Proxy
对象可以方便地调用对应的Reflect
方法, 完成默认行为, 作为修改行为的基础. 也就是说, 不管Proxy
怎么修改默认行为, 你总可以在Reflect
上获取默认行为.
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
对象中的方法 | 说明 |
---|---|
Reflect.apply() | 对一个函数进行apply 调用 |
Reflect.construct() | 对构造函数进行new 操作 |
Reflect.defineProperty() | 定义一个属性 |
Reflect.deleteProperty() | 删除一个属性 |
Reflect.get() | 获取一个属性 |
Reflect.getOwnPropertyDescriptor() | 获取一个属性描述符 |
Reflect.getPrototypeOf() | 获取一个对象的原型 |
Reflect.has() | 判断一个属性是否在对象中 |
Reflect.isExtensible() | 判断可以扩展 |
Reflect.ownKeys() | 获取一个对象中的key 集合 |
Reflect.preventExtensions() | 使一个对象不可扩展 |
Reflect.set() | 设置一个属性 |
Reflect.setPrototypeOf() | 设置一个对象的原型 |
7.3 小结
对于日益癫狂(登录, 关注博主可阅读, 关注公众号可阅读, 禁止复制, 禁止右键菜单, 禁止console, 各种版权声明, vip, vvip...)的网站, 元编程让用户可以拥有反制网站的手段.
// 以csdn为例, 该站点禁止复制内容(非登录)
// 调试, 找到其复制事件将触发在全局对象csdn => copyright下的function
unsafeWindow.csdn = {};
Object.defineProperty(unsafeWindow.csdn, "copyright", {
value: null,
});
// 在Tampermonkey中, 创建上述代码, 并设置为document-start的启动方式
// 将csdn中禁止复制代码(非登录的状态下)的拦截去掉
八. 类(class)
JavaScript 也有一个new 操作符, 使用方法看起来也和那些面向类的语言一样, 绝大多数开发者都认为JavaScript 中new 的机制也和那些语言一样. 然而, JavaScript 中new 的机制实际上和面向类的语言完全不同.
首先我们重新定义一下JavaScript 中的" 构造函数" . 在JavaScript 中, 构造函数只是一些使用new 操作符时被调用的函数. 它们并不会属于某个类, 也不会实例化一个类. 实际上, 它们甚至都不能说是一种特殊的函数类型, 它们只是被new 操作符调用的普通函数而已.
- < 你不知道的JavaScript >
使用new 来调用函数, 或者说发生构造函数调用时, 会自动执行下面的操作.
- 创建( 或者说构造) 一个全新的对象.
- 这个新对象会被执行[[ 原型]] 连接.
- 这个新对象会绑定到函数调用的this.
- 如果函数没有返回其他对象, 那么new 表达式中的函数调用会自动返回这个新对象.
Javascript是一种基于对象( object-based) 的语言, 你遇到的所有东西几乎都是对象. 但是, 它又不是一种真正的面向对象编程( OOP) 语言, 因为它的语法中没有
class
( 类) .那么, 如果我们要把"属性"( property) 和"方法"( method) , 封装成一个对象, 甚至要从原型对象生成一个实例对象, 我们应该怎么做呢?
需要注意, 函数声明和类声明之间的一个重要区别在于, 函数声明会提升, 类声明不会.
> {
... class Test {
// 私有属性
... #private_name='test';
... #private_state = false;
... #private_counter = 0;
... constructor(counter){
... console.log('class init');
... this.#private_counter = counter;
... }
... // 私有方法
... #private_abc(){
... console.log('private method')
... }
... // 静态方法
... static abc(){
... console.log('static method');
... }
... instance_abc(){
... console.log('instance method');
... }
... // 读属性
... get name(){
... return this.#private_name
... }
... /**
... * @param {boolean} mode
... */
... // 设置属性
... set state(mode){
... this.#private_state = mode
... }
... };
... const test = new Test(0);
... test.instance_abc()
... Test.abc();
... console.log(test.name);
... test.state = true;
... }
class init
instance method
static method
test
true
// 传统 "class" 模式
const Test = {
private_name:'test',
private_state : false,
private_counter : 0,
// 可读属性
get name(){
return this.private_name
},
/**
* @param {boolean} mode
*/
// 设置属性
set state(mode){
this.private_state = mode
},
init(counter){
// do something
}
}
8.1 Prototype
直到昨天, 我读到法国程序员Vjeux的解释, 才恍然大悟, 完全明白了Javascript为什么这样设计.
下面, 我尝试用自己的语言, 来解释它的设计思想. 彻底说明白prototype对象到底是怎么回事. 其实根本就没那么复杂, 真相非常简单.
这部分内容理解起来比较绕, 继承与原型链 - JavaScript | MDN (mozilla.org).
以下内容理解起来并不直观, 很容易被搞蒙.
8.1.1 浏览器的差异
图: chrome86
图: edge115
8.1.2 解析
**备注: ** 遵循 ECMAScript 标准, 符号
someObject.[[Prototype]]
用于标识someObject
的原型. 内部插槽[[Prototype]]
可以通过Object.getPrototypeOf()
和Object.setPrototypeOf()
函数来访问. 这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性__proto__
访问器. 为在保持简洁的同时避免混淆, 在我们的符号中会避免使用obj.__proto__
, 而是使用obj.[[Prototype]]
作为代替. 其对应于Object.getPrototypeOf(obj)
.它不应与函数的
func.prototype
属性混淆, 后者指定在给定函数被用作构造函数时分配给所有对象实例的[[Prototype]]
. 我们将在后面的小节中讨论构造函数的原型属性.
javascript
中的对象有一个特殊的[[Prototype]]
内置属性, 其实就是对其他对象的引用. 几乎所有的对象在创建时 [[Prototype]] 都会被赋予一个非空的值.
- < 你不知道的javascript >
每个函数都会创建一个
prototype
属性, 这个属性是一个对象, 包含应该由特定引用类型的实例共享的属性和方法. 实际上, 这个对象就是通过调用构造函数创建的对象的原型. 使用原型对象的好处是, 在它上面定义的属性和方法都可以被对象实例共享. 原来在构造函数中直接赋给对象实例的值, 可以直接赋值给它们的原型.
- < javascript高级程序设计 >
首先需要知道的.prototype
这个属性, 只有function
才会有这个属性, 这是function
专属的, 在高版本的chrome
上看到的对象[[Prototype]]和function.prototype
需要有所区分.
Object.__proto__
(这个特性已被废弃, 但是在各类代码中还是很常见相关内容)
Object
实例的__proto__
访问器属性暴露了此对象的[[Prototype]]
( 一个对象或null
) .
__proto__
属性还可以在对象字面量定义中使用, 作为创建对象时设置对象[[Prototype]]
的一种替代方法, 而不是使用Object.create()
. 请参见: 对象初始化/字面量语法. 该语法已经标准化, 并且在实现中得到了优化, 与Object.prototype.__proto__
相当不同.
function test(){}
const t = new test()
typeof t;
"object"
typeof test;
"function"
test.prototype.constructor === test; // 函数test的实例对象的构造函数是test
true
// test(父类) => 子类 => 子类的构造函数 => 当然是指向父类这个函数本身
t.constructor === test; // 函数test的实例对象的构造函数是test
true
test.__proto__.constructor === Function; // 函数test的"原型"(这里这样理解, test是Function的实例)的构造函数是Function, 那么就相当于`函数Function的实例对象(test)的构造函数是Function`
true
// test父类(test在这里是以子类的身份出现)的构造函数那么显然是Function
t.__proto__.constructor === test; // test的实例对象t的原型的构造函数时test
true
t.__proto__ === test.prototype; // test的实例对象t的原型指向函数test的实例
true
test.__proto__ === Function.prototype;
true
test.prototype.__proto__ === Object.prototype;
true
test.__proto__ === Object.__proto__;
true
Object.__proto__ === Function.prototype // Function 和 Object的关系
true
Function.prototype.__proto__ === Object.prototype // 绕晕了
true
Object.prototype.__proto__ === null
true
8.1.3 小结
(图: 你不知道的JavaScript)
详细的解析, 见这篇文章, 作者将这个问题解释得很清楚: 帮你彻底搞懂JS中的prototype, __proto__与constructor( 图解) _js prototype_码飞_CC的博客-CSDN博客
-
__proto__
和constructor
属性是对象所独有的; -
prototype
属性是函数所独有的. 但是由于JS中函数也是一种对象, 所以函数也拥有__proto__
和constructor
属性. -
prototype
属性, 它是函数所独有的, 它是从一个函数指向一个对象. 它的含义是函数的原型对象, 也就是这个函数( 其实所有函数都可以作为构造函数) 所创建的实例的原型对象, 由此可知:f1.__proto__ === Foo.prototype
. -
prototype
属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法, 即f1.__proto__ === Foo.prototype
.> { ... function test(){ ... this.class_name = 'test'; ... } ... const a = new test(); ... const b = new test(); ... console.log(a.class_name); ... console.log(b.class_name); ... } test test > { ... class test { ... class_name = 'test'; ... } ... const a = new test(); ... const b = new test(); ... console.log(a.class_name); ... console.log(b.class_name); ... } test test
-
__proto__
属性的作用就是当访问一个对象的属性时, 如果该对象内部不存在这个属性, 那么就会去它的__proto__
属性所指向的那个对象( 父对象) 里找, 一直找, 直到__proto__
属性的终点null, 再往上找就相当于在null上取值, 会报错. 通过__proto__
属性将对象连接起来的这条链路即我们所谓的原型链. -
constructor
属性的含义就是指向该对象的构造函数, 所有函数( 此时看成对象了) 最终的构造函数都指向Function. -
[JS 中 proto 和 prototype 存在的意义是什么? - 知乎 (zhihu.com)](https://blog.csdn.net/qq_38722097/article/details/88046377)
8.2 super
super 关键字用于访问对象字面量或类的原型( [[Prototype]]) 上的属性, 或调用父类的构造函数.
super.prop
和super[expr]
表达式在类和对象字面量任何方法定义中都是有效的.super(...args)
表达式在类的构造函数中有效.super([arguments]) // 调用父类的构造函数 super.propertyOnParent super[expression]
super
关键字有两种使用方式: 作为" 函数调用" (super(...args)
) , 或作为" 属性查询" (super.prop
和super[expr]
) .在派生类的构造函数体中( 使用
extends
) ,super
关键字可以作为" 函数调用" (super(...args)
) 出现, 它必须在使用this
关键字之前和构造函数返回之前被调用. 它调用父类的构造函数并绑定父类的公共字段, 之后派生类的构造函数可以进一步访问和修改this
.
class Polygon {
constructor(height, width) {
this.name = 'Rectangle';
this.height = height;
this.width = width;
}
sayName() {
console.log('Hi, I am a ', this.name + '.');
}
get area() {
return this.height * this.width;
}
set area(value) {
this._area = value;
}
}
class Square extends Polygon {
constructor(length) {
this.height; // ReferenceError, super 需要先被调用!
// 这里, 它调用父类的构造函数并传入 length
// 作为 Polygon 的 height, width
super(length, length);
// 注意: 在派生的类中, 在你可以使用 'this' 之前, 必须先调用 super().
// 现在可以使用 'this' 了, 忽略 'this' 将导致引用错误( ReferenceError)
this.name = 'Square';
}
}
// 调用实例方法和静态资源
class Base {
static baseStaticField = 90;
baseMethod() {
return 10;
}
}
class Extended extends Base {
extendedField = super.baseMethod(); // 10
static extendedStaticField = super.baseStaticField; // 90
}
字面量构建的类也可以用.
const obj1 = {
method1() {
console.log('method 1');
}
}
const obj2 = {
method2() {
super.method1();
}
}
Object.setPrototypeOf(obj2, obj1);
obj2.method2(); // logs "method 1"
由于Javascript
中的extends
不支持多重继承, 和Python
的super
相比,. 其feature
相对较少一些.56
>>> class Parent:
... def __init__(self):
... print('init')
...
>>>
>>> class Sona(Parent):
... def __init__(self):
... super().__init__()
... print('sona')
...
>>>
>>> class Sonb(Parent):
... def __init__(self):
... super().__init__()
... print('sonb')
...
>>>
>>> class Com(Sona, Sonb):
... def __init__(self):
... super().__init__() # 减少Parent的初始化次数
... print('com')
...
>>>
>>> c = Com()
init
sonb
sona
com
8.3 继承
class ChildClass extends ParentClass { ... }
{
class animal{
name='animal';
say(content){
console.log(this.name + ": " + content);
}
}
class dog extends animal{
name = 'dog'
}
class rabbit extends animal{
name = 'rabbit'
}
const d = new dog();
d.say('from dog');
const r = new rabbit()
r.say('from rabbit');
}
// dog: from dog
// rabbit: from rabbit
使用extend
可以对内置的对象进行改写:
{
// 对数据, 添加一个新的功能
class my_array extends Array{
// 返回数组的和的属性
get my_sum(){
return this.reduce((e, i) => i += e)
}
}
const a = new my_array(1,2,3);
console.log(a.my_sum);
}
// 6
Array
构造函数会根据给定的元素创建一个 JavaScript 数组, 但是当仅有一个参数且为数字时除外( 详见下面的arrayLength
参数) . 注意, 后者仅适用于用 Array 构造函数创建数组, 而不适用于用方括号创建的数组字面量.
{
// 对某些功能进行修改
window.Array = class my_array extends Array{
// constructor(...args){
// console.log('init');
// super(...args);
// }
push(arg) {
console.log(arg);
if (arg < 3) throw new SyntaxError('the number is too small');
else return super.push(arg);
}
}
const b = new Array(7, 8, 9);
// 但是这种模式无法对于字面量方式创建的数组进行拦截
const a = [7, 8, 9];
a.push(4);
console.log(a);
a.push(1);
console.log(a);
b.push(1);
}
VM257:16 (4) [7, 8, 9, 4]
VM257:18 (5) [7, 8, 9, 4, 1]
VM257:8 1
VM257:9 Uncaught SyntaxError: the number is too small
at my_array.push (<anonymous>:9:32)
at <anonymous>:19:7
使用Proxy
修复上面的字面量创建数组无法拦截的问题.
{
// 上面的new array无法拦截字面量创建的数组, 这里使用Proxy来实现拦截
window.Array.prototype.push = new Proxy(window.Array.prototype.push, {
apply(...args){
console.log(args);
const p = args[args.length - 1][0];
if (p > 1) {
console.log('push')
Reflect.apply(...args);
}else console.log('the number is too small')
},
});
const a = [1];
a.push(0);
console.log(a);
}
综上, 改写原函数的方式, 还可以有以下的变换.
> {
// 继承类, 影响所有通过new初始化的对象
... class my_array extends Array{
... includes(e){
... console.log('class');
... const i = a.indexOf(e);
... return i > 2;
... }
... }
... const a = new my_array();
... console.log(a.includes(1));
... }
class
false
> {
// 改变单一对象, 影响自身
... const a = [];
... a.includes = (e) => {
... console.log('self');
... const i = a.indexOf(e);
... return i > 2;
... };
... console.log(a.includes(1));
... }
self
false
> {
// 改变所有的数组, 影响全部形式的数组, 不管是字面量还是new
... Array.prototype.includes = (e) => {
... console.log('prototype');
... const i = a.indexOf(e);
... return i > 2;
... }
... const a = [];
... console.log(a.includes(1));
... }
prototype
false
但是这里对于数组的继承有一个问题, super.splice
出现第二次调用构造函数的情况.
{
class Test extends Array {
#name = null;
constructor(data, name) {
console.log('a');
if (typeof data !== 'object') {
super();
return;
}
super(...data);
this.#name = name;
}
remove_a() {
console.log('r');
super.splice(0, 1);
}
pop_a() {
console.log('p');
super.pop();
}
find_a(v) {
console.log('f');
const i = super.indexOf(v);
return i;
}
}
const a = new Test([1, 2, 3], 'a');
console.log(a.find_a(2));
a.pop_a();
a.remove_a();
console.log(a);
}
{
class Test extends Array {
#name = null;
constructor(data, name) {
console.log('a');
console.log(data, name);
if (typeof data !== 'object') {
super();
return;
}
super(...data);
this.#name = name;
}
remove_a() {
console.log('r');
//
console.log(super.splice(0, 1)); // 在原数组的基础上进行修改, 返回包含元素的相同结构的数组
console.log(super.toSpliced(0,1)) // 生成新的数组, 但是是普通的数组
}
pop_a() {
console.log('p');
super.pop();
}
find_a(v) {
console.log('f');
const i = super.indexOf(v);
return i;
}
}
const a = new Test([1, 2, 3], 'a');
a.remove_a();
}
可以看到splice
函数返回的依然是Test
结构的扩展数组, 应该是这个原因导致需要二度访问constructor
函数的问题.
九. 弱引用/Map/Set
这部分的内容由于是新增加的比较"高阶"的内容, 缺少相关的示例和更多的细节.
弱, weak
, 为什么需要weak
?
> {
... let a = {'a': 1};
... const b = [];
... b.push(a);
... a = null; // 按道理a = null;的操作是希望内存回收
... console.log(b); // 但是实际, 内存中依然还保持
... }
[ { a: 1 } ]
JavaScript 引擎在值" 可达" 和可能被使用时会将其保持在内存中
假如在实际的操作中, 出现大量类似的情况, 那么就会导致内存溢出的问题.
weak
, 应运而生, 以解决这种情况导致的内存无法有效回收的问题.
9.1 WeakRef
WeakRef 对象允许您保留对另一个对象的弱引用, 而不会阻止被弱引用对象被 GC 回收.
WeakRef 对象包含对对象的弱引用, 这个弱引用被称为该 WeakRef 对象的 target 或者是 referent. 对对象的弱引用是指当该对象应该被 GC 回收时不会阻止 GC 的回收行为. 而与此相反的, 一个普通的引用( 默认是强引用) 会将与之对应的对象保存在内存中. 只有当该对象没有任何的强引用时, JavaScript 引擎 GC 才会销毁该对象并且回收该对象所占的内存空间. 如果上述情况发生了, 那么你就无法通过任何的弱引用来获取该对象.
正确使用 WeakRef 对象需要仔细的考虑, 最好尽量避免使用. 避免依赖于规范没有保证的任何特定行为也是十分重要的. 何时, 如何以及是否发生垃圾回收取决于任何给定 JavaScript 引擎的实现. GC 在一个 JavaScript 引擎中的行为有可能在另一个 JavaScript 引擎中的行为大相径庭, 或者甚至在同一类引擎, 不同版本中 GC 的行为都有可能有较大的差距. GC 目前还是 JavaScript 引擎实现者不断改进和改进解决方案的一个难题.
class Counter {
constructor(element) {
// Remember a weak reference to the DOM element
this.ref = new WeakRef(element);
this.start();
}
start() {
if (this.timer) {
return;
}
this.count = 0;
const tick = () => {
// Get the element from the weak reference, if it still exists
const element = this.ref.deref();
if (element) {
element.textContent = ++this.count;
} else {
// The element doesn't exist anymore
console.log("The element is gone.");
this.stop();
this.ref = null;
}
};
tick();
this.timer = setInterval(tick, 1000);
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = 0;
}
}
}
const counter = new Counter(document.getElementById("counter"));
counter.start();
setTimeout(() => {
document.getElementById("counter").remove();
}, 5000);
9.2 WeakMap
WeakMap
对象是一组键/值对的集合, 其中的键是弱引用的. 其键必须是对象, 而值可以是任意的.WeakMap 的 key 只能是
Object
类型. 原始数据类型 是不能作为 key 的( 比如Symbol
) .
// key必须是对象
(new WeakMap).set('test', 'test');
VM209:1 Uncaught TypeError: Invalid value used as weak map key
at WeakMap.set (<anonymous>)
at <anonymous>:1:15
class Test {
constructor() {
// 生成占用明显一点数据
this.data = new Array(2 * 1024 * 1024);
}
}
const m = new Map();
let test = new Test();
m.set(test, 'data');
test = null;
执行代码 => Memery
=> 点击小按钮, 查找test
, 可以看到未被回收的内存占用.
class Test {
constructor() {
// 生成占用明显一点数据
this.data = new Array(2 * 1024 * 1024);
}
}
const m = new WeakMap();
let test = new Test();
m.set(test, 'data');
test = null;
WeakSet
和WeakMap
的差不多, 都是充当类似的容器, 不展示具体例子.
WeakSet
对象允许你将弱保持对象存储在一个集合中.
WeakSet
对象是一些对象值的集合. 且其与Set
类似,WeakSet
中的每个对象值都只能出现一次. 在WeakSet
的集合中, 所有对象都是唯一的.它和
Set
对象的主要区别有:
WeakSet
只能是对象的集合, 而不能像Set
那样, 可以是任何类型的任意值.WeakSet
持弱引用: 集合中对象的引用为弱引用. 如果没有其他的对WeakSet
中对象的引用, 那么这些对象会被当成垃圾回收掉.
十. 其他
10.1 类型提示
JavaScript
和Python不
同, JavaScript
原生还尚未支持变量类型提示辅助语法.
>>> def test(a: int, b: str) -> tuple[int, str]:
... a +=1
... b += 'abc'
... return a, b
...
>>> print(test(1, 'hello'))
(2, 'helloabc')
TypeScript中文网 - TypeScript- - JavaScript的超集 (tslang.cn), 是一个好的选择.
{
const a: string = 'abc';
const test = (data: number[]): number => data.reduce((e, i) => i += e);
}
10.2 TamperMonkey
Tampermonkey is one of the most popular browser extension with over 10 million users. It's available for Chrome, Microsoft Edge, Safari, Opera Next, and Firefox.
It allows its users to customize and enhance the functionality of your favorite web pages. Userscripts are small JavaScript programs that can be used to add new features or modify existing ones on web pages. With Tampermonkey, you can easily create, manage, and run these userscripts on any website you visit.
强大到极致的脚本管理器.
10.3 Python调用JavaScript
需要依赖于Node.js
, 这也是目前运行JavaScript
代码的最佳实践, 执行速度快, 兼容性完全不存在问题.
附带参数运行命令并返回其输出.
如果返回码非零则会引发
CalledProcessError
.CalledProcessError
对象将在returncode
属性中保存返回码并在output
属性中保存所有输出.这相当于:
run(..., check=True, stdout=PIPE).stdout
subprocess.check_output(
args,
*,
stdin=None,
stderr=None,
shell=False,
cwd=None,
encoding=None,
errors=None,
universal_newlines=None,
timeout=None,
text=None,
**other_popen_kwargs
)
from subprocess import check_output
# 简单的测试
def test(code):
'''
main(3, 2).toString()
mian(), 需要调用的函数名称
3, 2, main函数需要传递的参数
toString(), 保证输出为字符串
'''
code += 'process.stdout.write(main(3, 2).toString())'
r = check_output('node', input=code, universal_newlines=True, timeout=100)
print(r)
if __name__ == '__main__':
add = r'''
function main(x, y) {
return x + y;
}
'''
sub = r'''
// ES6+ 箭头函数
const main = (x, y) => x - y;
'''
test(add) # 5
test(sub) # 1
# js文件
js_file = ''
with open(js_file, mode='r', encoding='utf-8') as f:
test(f.read())
10.4 脚本的类型
在ublock origin
的操作板
- 内联脚本, 即写在
html
内部的脚本代码 - 第一方脚本, 即网站自身的外部脚本文件
- 第三方脚本, 这个最常见就是各种第三方库, 或者广告脚本.
属性 | 值 | 描述 |
---|---|---|
asyncNew | async | 规定异步执行脚本( 仅适用于外部脚本) . |
charset | charset | 规定在脚本中使用的字符编码( 仅适用于外部脚本) . |
defer | defer | 规定当页面已完成解析后, 执行脚本( 仅适用于外部脚本) . |
src | URL | 规定外部脚本的 URL. |
type | MIME-type | 规定脚本的 MIME 类型. |
xml:space | preserve | HTML5 不支持. 规定是否保留代码中的空白. |
type
值 | 描述 |
---|---|
MIME_type | 规定脚本的 MIME 类型. 一些常用的值: text/javascript ( 默认) 请参阅 IANA MIME 类型 , 获得标准 MIME 类型的完整列表. |
text/ecmascript | |
application/ecmascript | |
application/javascript | |
text/vbscript |
内联脚本
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test_which window</title>
<script>
// 内联脚本
var val = 'test_main';
setTimeout(() => {
console.log(val);
}, 500);
</script>
</head>
<body>
<h1>main_window</h1>
<script type="text/javascript" data-rms="1" src="a.js" nonce=""></script> <!-- 第一方脚本 -->
<script type="text/javascript" data-rms="2" src="b.js" nonce=""></script>
<script src="https://cdn.staticfile.org/clipboard.js/2.0.11/clipboard.js"></script> <!-- 第三方脚本 -->
</body>
</html>
第一方脚本
// a.js
var val = 'a_val';
console.log(val);
// b.js
var val = 'b_val';
console.log(val);
创建一个test
, 创建上述代码的三个文件.
a.js:2 a_val
b.js:2 b_val
index.html:10 b_val
注意全局同名变量交叉污染的问题.
let val = 'test_main';
setTimeout(() => {
console.log(val);
}, 500);
Uncaught SyntaxError: Identifier 'val' has already been declared (at a.js:1:1)
at a.js:1:1
(anonymous) @ a.js:1
b.js:1 Uncaught SyntaxError: Identifier 'val' has already been declared (at b.js:1:1)
at b.js:1:1
(anonymous) @ b.js:1
index.html:10 test_main
10.5 调包
令人遗憾的是, JavaScript由于各种问题, 调包的实现颇为麻烦, 做不到开盖即用, 前端 - 瞎折腾: 在html中使用 require/exports 或 import/export - 个人文章 - SegmentFault 思否.
下面还是以上面的文件为例子, 稍微修改.
<!DOCTYPE html>
<!-- index.html -->
<html>
<head>
<meta charset="utf-8">
<title>Test_which window</title>
<script>
let val = 'test_main';
setTimeout(() => {
console.log(val);
}, 500);
</script>
</head>
<body>
<h1>main_window</h1>
<!-- "text/javascript"-->
<!--type="module", 必须修改成这个模式, 否则报错-->
<script type="module" data-rms="1" src="./a.js" nonce="" crossorigin="anonymous"></script>
</body>
</html>
// a.js
import { test } from './b.js'
test('a');
// b.js
export const test = (arg) => console.log(arg);
Access to script at 'file:///D:/common_code/javascript/test/a.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome-untrusted, https, edge.
假如直接访问将会出现如上报错, 因为安全原因: javascript - Getting error "Strict MIME type checking is enforced for module scripts per HTML spec" when trying to import project - Stack Overflow.
The thing is: if you load the main HTML file from the local file system, all other resources are also requested with the file://
protocol; however the HTML spec prohibits loading ES modules with such protocol for security reasons (more info here).
需要搭建一个建议的本地服务器, 这里使用node.js
, http-server - npm (npmjs.com).
npm install --global http-server
cd D:\common_code\javascript\test
# 进入文件所在文件夹
http-server # 启动服务器
访问上述的地址, 由于是目标文件夹中打开的, 且存在index.html
文件, 将直接访问该页面, 可以看到调包操作可以正常运行.
十一. 参考信息
11.1 站点
11.2 书籍
- < 阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版 >
- < 你所不知道的JavaScript >