js 基础&进阶 速查笔记

笔记

个人笔记!不是文档!大家 尽量减少个人文档行为,多做公共wiki和文档翻译!

感谢

https://zh.javascript.info

https://javascript.info

有精力的朋友还是建议读上面链接,如果它们的文档有帮助,也建议购买它们的 epub/pdf 作为支持

本文是基于我C++的熟练,和一定时间js使用后的阅读知识补充整理,章节分化基本和上面链接的内容对应

JavaScript 基础知识 2

函数 2.15

函数表达式 vs 函数声明 2.16

function a(){} vs a = function(){}

当 JavaScript 准备运行脚本或代码块时,它首先在其中查找函数声明并创建函数。我们可以将其视为“初始化阶段”。

在处理完所有函数声明后,执行继续。

所以以下可以而正常运行

1
2
3
4
5
sayHi("John"); // Hello, John

function sayHi(name) {
alert( `Hello, ${name}` );
}

声明还是有代码块的问题

分号

1
2
3
alert("There will be an error after this message")

[1, 2].forEach(alert)

上面会报错,因为编译器不确定加分号,导致

alert()[1,2].forEach() , 其中1,2视作 逗号表达式返回2,也就是取alert()返回数组的第二个元素

代码质量 3

https://zh.javascript.info/ninja-code 这一章吐槽真的有趣23333

测试 3.5

同时适用于浏览器端和服务器端!

Mocha —— 核心框架:提供了包括 describe 和 it 的通用型测试函数和运行测试的主函数。

Chai —— 提供很多断言支持的库。它可以用很多不同的断言。现在我们只需要用 assert.equal。Chai doc

Sinon —— 用于监视函数、模拟内置函数和其他函数的库,我们稍后会用到它。

补充,现在还有可选的jasmine和karma

例子

1
2
3
4
5
6
7
describe("pow", function() {

it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
});

});

一个测试检测一个东西。

https://zh.javascript.info/testing-mocha 上面的demo 有点长,但很好的,建议直接看网页

before/after and beforeEach/afterEach测试前后包围

规范先行,实现在后。一个有良好测试的代码通常都有更好的架构。

测试规范,实际上 不应该在测试中 使用”运算“,否则可能需要 调试测试

经过 具体化以后的测试,还可以使用it()->it.only()进行单个测试

Polyfill 3.6

ES6 支持

https://kangax.github.io/compat-table/es6/

Babel

它将现代的 JavaScript 代码转化为上一代的标准形式。

脚本有一个术语 “polyfill” 表示用来“填补”缺口并添加缺少的实现。

两个有意思的 polyfills 是:

  • babel polyfill 支持很多,但是很大。
  • polyfill.io 服务允许我们能根据自己所需的特性来按需加载、构建 polyfill。

除了针对旧的版本,反过来,还有https://www.google.com/chrome/canary/ 的为开发人员准备的未来版本,每日构建版本,不支持Linux?????? 操了

https://groups.google.com/a/chromium.org/forum/#!msg/chromium-discuss/GNE4pEXLMkQ/cis32ewLosYJ

Objects(对象):基础知识 4

对象 4.1

计算属性

1
2
3
let bag = {
[fruit+'2333']: 5, // 属性名从 fruit 变量中计算 一般只有抽象出方法变量并且用于ide推断辅助时才用这个语法
};

特殊属性

__proto__ 历史原因,不能设置为 非对象 值

属性值简写(不建议)

1
2
3
4
5
6
7
function makeUser(name, age) {
return {
name, // 与 name: name 相同
age // 与 age: age 相同
// ...
};
}

for…in

和标题有点对不上,主要是 string和number转换相关

字符串整数属性会相互转换,用加号控制

1
2
3
4
5
6
7
8
9
10
11
let codes = {
"+49": "Germany", //用加号 保护字符串不会被转化为数字
"+41": "Switzerland",
"+44": "Great Britain",
// ..,
"+1": "USA"
};

for(let code in codes) {
alert( +code ); // 49, 41, 44, 1 // 这里感觉有问题,说是去加号,实际是转化成数值,但是如果值过大汇编成科学计数法,所以正确的应该还是字符串的去加号方法
}

引用复制(值和引用) 4.2

原始类型是:字符串,数字,布尔类型 – 是被整个赋值的。

其它是引用

Object.assign 只是深度为1的深拷贝,要完全深拷贝,还是要自己实现,我喜欢在这里出面试题,深拷贝深比较

垃圾回收 4.3

let

变量没有let就是全局

Symbol 类型 4.7

对象的属性键只能是 String 类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有 String 或 Symbol 这两种类型。

Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个不影响任何东西的标签。

Symbol 不能隐式的转换为String,要转化的话只能用 toString()

Symbol 在 for…in 中被跳过

Symbol 在 Object.assign 中不会被忽略

全局注册表用法

let id = Symbol.for(“id”)

返回key

Symbol.keyFor(Symbol.for(“test”))

从技术上说,Symbol 不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 Symbol。还有一个名为 Reflect.ownKeys(obj) 返回所有键,包括 Symbol。所以它们不是真正的隐藏。但是大多数库、内置方法和语法结构都遵循一个共同的协议。而明确调用上述方法的人可能很清楚他在做什么。

well known symbol

对象方法与this 4.4

this运作方式

其实真的要讲就一句话,this在运行时才被复为具体的值,所以看运行到解释器去解释this那一行时的运行主体是谁即可。(这里忽略了一些编译器优化 可能发生的直观和实际造成的差异)

从根本上,我们是要去避免它带来的问题,而不是掌握它的各种用法。

怎样选择合适的实体?如何组织它们之间的交互?这就是架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let user = { name: "John" };
let admin = { name: "Admin" };

// 这样的代码没有语法错误:
function sayHi() {
alert( this.name );
}
sayHi(); // 严格模式(use strict)下是undefined,非严格模式下是window

// 在两个对象中使用的是相同的函数
user.f = sayHi;
admin.f = sayHi;

// 它们调用时有不同的 this 值。
// 函数内部的 "this" 是点之前的这个对象。
user.f(); // John (this == user)
admin.f(); // Admin (this == admin)

admin['f'](); // Admin(使用点或方括号语法来访问这个方法,都没有关系。)

this 是在运行时求值的。它可以是任何值。在运行时对 this 求值的这个想法有其优缺点。一方面,函数可以被重用于不同的对象。另一方面,更大的灵活性给错误留下了余地。这里我们的立场并不是要评判编程语言的这个想法的好坏,而是要了解怎样使用它,如何趋利避害。

要深入理解 obj.method() 调用的原理。

  1. 首先,点 ‘.’ 取得这个 obj.method 属性。
  2. 其后的括号 () 调用它。

错误示例

1
2
3
4
5
6
7
8
9
10
let user = {
name: "John",
hi() { alert(this.name); },
bye() { alert("Bye"); }
};

user.hi(); // John (the simple call works) 为了让 user.hi() 有效,JavaScript 用一个技巧 —— 这个 '.' 点返回的不是一个函数, 而是一种特殊的[引用类型]的值(https://tc39.github.io/ecma262/#sec-reference-specification-type).

// 现在我们要判断 name 属性,来决定调用 user.hi 或是 user.bye。
(user.name == "John" ? user.hi : user.bye)(); // Error!

实际为

1
2
let XXX = (user.name == "John" ? user.hi : user.bye);
XXX(); // Error! 因为XXX没有this

引用类型 (base, name, strict)三元组 ,其中strict为bool,与是否use strict相关

上面user.hi() (user['hi']同理)在严格模式下,对应(user,'hi',true)

而上面的XXX = user.hi过程中会丢掉user,只取函数,所以 一个函数,要么是一个单纯的函数,要没么是一个上面的三元组

1
2
3
4
5
6
user.hi() // 直接的调用
(user.hi)() // 解析过程去括号 不算赋值,传递三元组引用
(x=user.hi)() // 会在赋值时去掉user实体
(1==1?user.hi:alert)() // 并没有看到 解析会自动简化
(true?user.hi:alert)() // +1
(user.hi || alert)() // +1
1
2
3
4
5
6
7
8
9
function makeUser() {
return {
name: "John",
ref1: this, // 会在上面的return的时候计算,这时候没有this ,所以 (a = makeUser())['ref1'] 是undefined
ref2: function(){ // 在执行时才计算 (a = makeUser())['ref2']() 得到本体
return this; // 不会预先算
}
};
};

箭头函数没有自己的this

箭头函数使用外部正常的函数的this

1
2
3
4
5
6
7
8
9
let user = {
firstName: "Ilya",
sayHi() {
let arrow = () => alert(this.firstName);// 替换为function(){alert(this.firstName);}后变为undefined
arrow();
}
};

user.sayHi(); // Ilya

对象原始值转换 4.8

Symbol.toPrimitive String Number

相对于c++ 的重载operator,

Symbol.toPrimitive

当一个对象被用在需要原始值的上下文中时,例如,在 alert 或数学运算中,它会使用 ToPrimitive 算法转换为原始值

1
2
(x = [2,3,3])[Symbol.toPrimitive] = function(hint){console.log(`HINT[${hint}]`);}
alert(x);

从例子,首先能看到,是会调用到 该函数,第二能看到hint传递过来的是string

string

当一个操作期望一个字符串时,对于对象到字符串的转换

alert(obj)anotherObj[obj] = XXX;

toString -> valueOf

number

当一个操作需要一个数字时,用于对象到数字的转换,如 maths

1
2
3
4
5
6
7
8
9
// 显式转换
let num = Number(obj);

// maths(除了二进制加法)
let n = +obj; // 一元加法
let delta = date1 - date2;

// 小于/大于的比较
let greater = user1 > user2;

valueOf -> toString

default 未知状况

例如 字符串+数字,是采用链接字符串,或数值相加,,这里说了一堆,大概意思是,default 可以视为和number 状况一致

string+number 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let user = {
name: "John",
money: 1000,

// 对于 hint="string"
toString() {
return `{name: "${this.name}"}`;
},

// 对于 hint="number" 或 "default"
valueOf() {
return this.money;
}

};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500 假设valueOf不存在,则采用toString()

运作方式总结

为了进行转换,JavaScript 尝试查找并调用三个对象方法:

  1. 调用 objSymbol.toPrimitive 如果这个方法存在的话,

  2. 否则如果暗示是 “string”

  3. 尝试 obj.toString() 和 obj.valueOf(),无论哪个存在。

  4. 否则,如果暗示 “number” 或者 “default”

  5. 尝试 obj.valueOf() 和 obj.toString(),无论哪个存在。

以上三个方法,不规定返回对应值,但一定要返回原始值[强制的],[否则 有潜在递归可能性, 比如A.toString()返回类型A]

一点区别

toString/valueOf 如果返回的不是原始值,则会被忽略该方法

Symbol.toPrimitive 如果返回的不是原始值,则会报错被忽略该方法

构造函数和操作符new

约定首字母大写…我们谈语法时别谈约定,可以说建议,如果代码一定要就eslint什么的搞一搞

1
2
3
4
5
6
7
function user(name) {
// this = {}; 隐方创建
this.name = name;
// return 2333; // 如果在过程中有return 会从这里停止,但是如果返回的是原始值(包括返回空),如这里的2333,则忽略这个值,返回this,如果返回值是object,则忽略this,返回object
this.fuckable = false;
// return this; 隐式返回
}

当一个函数作为new user(...) (没有参数时,可以省略括号)执行时,它执行以下步骤:

  1. 一个新的空对象被创建并分配给 this。
  2. 函数体执行。通常它会修改 this,为其添加新的属性。
  3. 返回 this 的值!!!

单次使用x = new function(){ ... }

1
2
3
4
5
6
7
8
9
10
function User(name) {
if (!new.target) { // 如果你没有运行 new
return new User(name); // ...会为你添加 new
}

this.name = name;
}

let john1 = User("John1"); // 重新调用 new User
let john2 = new User("John2"); // 正常的使用方法

但是不建议这样做,这样会对代码的阅读造成困扰

Optional chaining ‘?.’ 4.6

觊觎这个很久了,感觉还是历史遗留问题的 半 优化,没有从根本上解决问题。

虽然感觉根本办法还是应该在 和 其它端交互时进行 强制的状态和空值处理,TS基本考虑100%上,然后可以考虑用protobuf替代json。

数据类型 5

吐槽:看到IE 浏览器不支持这个方法就想笑

基本类型

  1. Boolean
  2. Null
  3. Undefined
  4. Number
  5. String
  6. Symbol (new in ECMAScript 6)

基本类型在作为对象使用时,会临时创造一个包装对象,使用完后即销毁

1
2
3
4
a= 3
a.wasd=()=>233; //不会报错,因为a是基本类型,但是是放在上面说的临时对象上了
a.wasd();// 报错 a.wasd不是function
a.toFixed(2); // 基本类型 带有一些基本的方法

哎 js还不支持c++14支持的a=1'000'000'000这种写法

toString(base) 可以进行进制转换 base 取值2->36

isNaN / isFinite

之前看vuex源码时 ,看到很喜欢用new 来建立空的对象、数组什么的,难道是英文友好性,比如我一般写a=[]而他们可能就会写a=new Array()

数组=队列 支持 shift/unshift (首部添加 慢) 和 pop/push (尾部添加 较快)

数组引用的方式复制

遍历 for i =0 -> arr.length, for key in arr (会迭代所有属性,这里说这还会慢..10-100倍 所以遍历数组通常不用forin), for value of arr

性能测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let arr = new Array();
for(let i = 0, len = 10000000;i < len; i++){
arr.push(i);
}

function foriadd(my_arr){
let sum = 0;
for(let i = 0; i < my_arr.length; i++){
sum += my_arr[i];
}
}

function forinadd(my_arr){
let sum = 0;
for(let key in my_arr){
sum += my_arr[key];
}
}

function forofadd(my_arr){
let sum = 0;
for(let val of my_arr){
sum += val;
}
}

function timeTest(func,my_arr,str) {
let start_time = new Date().getTime();
func(my_arr);
let end_time = new Date().getTime();
console.log(str,(end_time - start_time).toString());
}
timeTest(foriadd,arr,'foriadd'); // 17
timeTest(forinadd,arr,'forinadd'); // 3702 之所以慢 据说因为会引起和string之间的转换
timeTest(forofadd,arr,'forofadd'); // 150

修改数组时,length 属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。(慎用)

length 属性的另一个有意思的点是它是可写的。把它变小,数组真的可以变短,所以可以用arr.length=0来清空..(为什么这里说这是清空数组的最好的方法?????)…….. 危险的语言

警告 new Array(xxxx), 当xxxx是字符obj等东西的时候 实际创建的是 包含对应元素的数组,而是数字时,表示的是 对应长度的空数组

delete arr[1]; // 如果arr的长度为5(大于2),会删除index==1的元素,但是 不会把后面的向前移动,也不会影响length

arr.splice(index[,deleteCount,elem1,…,])

-1的index 表示从尾部计算

还有支持简单参数的slice,

….说这么多 ,简单的说 数组别乱玩,

一些concat函数等就不说了,要用啥查啥

1
2
3
4
5
6
7
8
9
10
11
12
13
//map
let result = arr.map(function(item, index, array) {
// 返回新值而不是当前元素
})

// sort
[1, -2, 15, 2, 0, 8].sort(function(a, b) { // 更建议箭头函数 ,返回正负零,不是true/false,类似c++ qsort接受的函数指针
alert( a + " <> " + b );
});
// reduce , previousValue — 是前一个函数调用的结果,第一次调用是初始化。
let value = arr.reduce(function(previousValue, item, index, arr) {
// ...
}, initial);

判断一个变量是否是数组,不要用typeof,用Array.isArray()

1
2
3
4
5
arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg 是可选的最后一个参数

表示比较函数用的 2号参数,也就是 比较过程中采用func(数组中的元素,thisArg);

可迭代对象(Iterables)

一个含有 [Symbol.iterator] key的对象,实例

1
2
a = {x:1,y=2};
a[Symbol.iterator] = function() { return {current:this.x,last:this.y,next(){ return {done:false,value:this.current}; }}; }
  1. 需要Symbol.iterator
  2. 该函数返回值是个object (迭代器),该object需要带有next()方法
  3. next方法回传一个迭代器 {done:Boolean,value:any} 的形式,这样就可以支持,for...of

学过c++的就可以类比看看vector,string和它们的迭代器

Array.from(XXX) 方法从一个类似数组或可迭代对象中创建一个新的数组实例。

Map 可以用 对象作为key(区别于普通Object)………嗯嗯 好了好了 我知道了,说得c++好像不能用指针作为key一样

map的key比较是采用===, 然后map的set可以链式操作,因为返回本体

键值数组->new Map()->变为map

map/obj-> Object.entries() -> 变为键值数组 for..of会默认使用entries

js有Set, 迭代for..of 和 set.forEach

WeakMap/WeakSet 忽略内存回收的依赖检测

这里说的适用场景,是有主对象控制区,在辅助非必要信息区域可以采用WeakMap来存

比如

1
2
3
4
obj = {};
let weakMap = new WeakMap();
WeakMap.set(obj,"xxx") // obj位置的参数不能用原始类型
obj = null; // obj 会被回收,也会从weakMap从消失,而如果把WeakMap换成Map,则因为,还能在map中找到不会被回收

map.keys() 返回的是迭代器不是 数组,用Array.from(map.keys())可以转化成数组

对于obj,Object.keys(obj)来返回数组,而不是迭代器

Object.keys/values/entries (), 忽略Symbol类型的属性

数组/可迭代对象解构

let [a,b,c..] = arr/可迭代对象; // 自动分配到a,b,c

let [,,a,b,c..] = arr/可迭代对象; // 忽略前两个值

let [a,b,...rest] = arr/可迭代对象; // rest接受剩余元素

对象解构

let {key1,key2} = {key1:...,key2:...} 以key配对,不是顺序

let {key1:var1,key2:var2} = {key1:...,key2:...} 目标变量的写法

let {key1= 100,key2:var2} = {key1:...,key2:...} 缺省值写法

严格模式下,先let后解构,外部加括号包起来

1
2
let title, width, height;
({title, width, height} = {title: "Menu", width: 200, height: 100});

还有嵌套解构……..我觉得还行,感觉这也算得上半结构化了

然后函数传参可以用默认值+ 等于号的python里常看到的方法,假设有定义f(x=1,y=3,z=4){}, 调用f(z=233)

函数还可以写成 f({a=1,b=7,c=3}){}, 然后调用x={b=2};f(x)

这一块来说就很是灵活好用易读的感觉了

date.get你希望改变的时间维度()+数值

Date.now()相当于 new Date().getTime(), 只会创建时间戳,性能友好 更快!

浏览器方法performance.now() 微秒数

JSON 相关,内部不应该有循环引用

JSON.stringify(value [,replacer(替换函数设置),space(空格缩进换行符设置) ])

JSON.stringify(value ,[指定属性1,指定属性2,...])

replacer : function(key,value) {return 替换值;}

toJson 函数可以重写,用于被解析

目前非标准的https://json5.org/ 支持注释,无引号的键 !!

JSON.parse(data,function(key,value){return 定制化value;}),可以进行定制化处理

Advanced working with functions 6

任意长度参数 Rest 参数与 Spread 语法 6.2

function f(...args){} // 注意… 必须放在列表尾

或者通过内置的arguments访问(旧式的,这里没说不建议用)

箭头函数是没有 “arguments”,箭头函数内访问arguments等同于访问其外部非箭头函数的arguments

三点展开a=[2,3,3]; f(...a);// 展开a并调用f,可以放在句中, 展开和for .. of 采用相同展开对象

闭包

变量环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                                                        //     3
function makeCounter() { // 3
let count = 0; // 2 3
return function() { // 1 2 3
return count++; // has access to the outer counter // 1 2 3
}; // 1 2 3
} // 2 3
// 3

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

JavaScript中的所有函数都是闭包

if for等的花括号内的 都有此法环境

在旧的脚本中,我们可以找到一个所谓的『立即调用函数表达式』(简称为 IIFE)用于此目的。

(function(){ /*.CODE HERE.*/; })()

除此以外还有方式

(function(){ /*.CODE HERE.*/; }())

!function(){ /*.CODE HERE.*/; }()

+function(){ /*.CODE HERE.*/; }()

垃圾回收 与 闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function f() {
let value = Math.random();

function g() {
console.log(value); // 注释掉这一句查看变化
debugger; // 在控制台中输入 alert( value );没有该值!
}
function h() {
console.log(value);
}

return [g,h];
}

let [g,h] = f();
g();
h();

通过上面的实验可以看到,

在有第一个console的时候,我们可以在debugger的位置访问到value,没有的这个console则则不行,也就是这里可以看到js预测下的优化,省略掉一些认为不存在的值,

因为我们的h在g函数之后,还能够访问value,但是g的位置在注释掉第一个console则访问不到,说明这个地方并不是value被回收了,而是,value没有被传入

用 var 声明的变量(不建议 不建议 不建议),不是函数范围就是全局的,它们在块内是可见的。也就是花括号 不会限制var, var 在函数开始时执行,不论声明位置,甚至是这样[所有函数内的var的声明 都会被提升到顶部,但是赋值的顺序不会变]

1
2
3
4
5
6
7
8
9
function sayHi() {
phrase = "Hello"; // (*)

if (false) {
var phrase;
}

alert(phrase);
}

真的被使用了吗?如果你有疑问,可以去掉if以及var,你会发现在全局可以访问phrase,而加上if和var后,全局是访问不到phrase的,也就是说创建的是函数内的phrase,也就是var真的被执行了!!

在全局下使用let以后,这个变量也不是全局,只是”根”下可访问

全局的使用window.XMLHttpRequest检测类型,iframe 的正确窗口获取变量

函数

函数的.name能得到函数名a =function(){}; b={c(){}}; a.name;b.c.name;

而很多函数也没有函数名,d={e:function(){}};d.e.name; l=b.c;// 最后这种赋值函数指针会带上函数名一起复制

函数的.length表示接受的参数个数,其中...args在计数中被忽略

函数可以加上属性XD有时用来代替闭包

如下的func秀啊

  1. 它允许函数在内部引用自己。
  2. 它在函数外是不可见的。
1
2
3
4
5
6
7
8
9
10
11
12
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // 使用 func 再次调用自己 而不要使用sayHi,因为这里来说func不可变 sayHi是可变的
}
};

sayHi(); // Hello, Guest

// 但这个无法生效
func(); // Error, func is not defined(在函数外不可见)

嗯。。。。感觉这种就是个函数指针类似的玩法咯

可以还奶了一口jQuery和lodash

可以这样创建 函数,一下所有字段都是字符串 XD

let func = new Function ([arg1[, arg2[, ...argN]],] functionBody)

比如使用场景是,从服务器获取动态函数

但是new Function不论在哪里创建, 所指向的词法环境是全局,从一定程度上强迫写出好代码

1
2
3
new Function('a', 'b', 'return a + b'); // 基础语法
new Function('a,b', 'return a + b'); // 逗号分隔
new Function('a , b', 'return a + b'); // 逗号和空格分隔

setTimeout 和 setInterval

不在标准方法中不在规范中

取消调度

1
2
3
let timerId = setTimeout(...);
clearTimeout(timerId);
alert(timerId);

弹窗会让 Chrome/Opera/Safari 内的时钟停止!!!!!!???? 我的chromium 执行setInterval 如果不点,等一会,会连续冒出alert啊,难道chrome也改了??

setTimeout(…,0) 尽快,但是在当前代码执行完后紧接着执行

这里有一种slot+setTimeout0分割任务CPU占用方法

用 setTimeout 进行分割和没用这两种做法在速度方面平分秋色,总的计数过程所花的时间几乎没什么差别。我一个总耗时2s,一个总耗时8s,但是我把它们都 用nodejs去跑的确时间差不多,也就是8s的应该在过程中处理了其它事件耗时了

HTML5 标准 所言:“经过 5 重嵌套之后,定时器运行间隔强制要求至少达到 4 毫秒”。服务端 JavaScript 就没这个限制了

1
2
3
4
5
6
7
8
9
let start = Date.now();
let times = [];

setTimeout(function run() {
times.push(Date.now() - start); // 保存上次调用的延时

if (start + 100 < Date.now()) alert(times); // 100 毫秒之后,显示延时信息
else setTimeout(run, 0); // 没超过 100 毫秒则再进行调度
}, 0);

所有的调度方法都不能保证延时的准确性,所以在调度代码中,万不可依赖它。

优化 & bug fix

缓存包装函数,耗时久,但是输入输出稳定的,大量相同输入访问的函数的通用包装函数,见原文实现

func.call(context,...args) 解决包装后上下文丢失没有this

func.apply(context,args) 解决 多参数为一个数组

func(1,2,3) ≈ func.call(context,1,2,3) ≈ func.apply(context,[1,2,3])

// 这里是外部再接受一个自定义hash函数来对多参数计算一个key,arguments本身不是数组,不过可以[].join.call(arguments)来拼接

下面几个装饰器 任务 挺有用

对于延迟的this丢失 处理

let newFunc = func.bind(thecontextyouwant) 通过bind 指定this,只能bind一次,多次bind,第2次及以后的会被忽略

lodash 中的 _.bindAll(obj)。批量绑定

另一种方案就是箭头函数了

偏函数,函数的封装=.= lodash 大法好啊 有对应的_.partial

柯里化 把调用形式f(a,b,c)变为f(a)(b)(c)形式 lodash _.curry 用lodash的封装,柯里化之后 原来调用方法 依然正常运行:也可以柯里化格式调用

高级柯里化https://github.com/lodash/lodash/blob/4.1.1-npm-packages/lodash.curry/index.js

对象、类和继承 7&8

对象属性除value外还有三个特殊属性(所谓的“标志”):

  1. writable — 如果为 true,则可以修改,否则它是只读的。
  2. enumerable — 如果是 true,则可在循环中列出,否则不列出。for … in
  3. configurable — 如果是 true,则此属性可以被删除,相应的特性也可以被修改,否则不可以。使属性不可配置是一条单行道。我们不能把它改回去.非严格模式 写入被忽略,严格模式报错

Object.getOwnPropertyDescriptor(obj, propertyName); 方法允许查询有关属性的完整信息。

Object.defineProperty(obj, propertyName, descriptor) 可以修改标识

上面的两个方法均有带s的 多元素方法

如果用for..in复制对象,会忽略symbolic属性,这样可以 获取所有属性(“更好”的克隆)

还有一些属性(实践中不常用的) 见原文了

set v(){return val;} && get v(val){return this.xxx=val;} 可以只有其中一个, 意味着只读或只写

属性可以是“数据属性”或“访问器属性”,但不能同时属于两者。

访问器没有value和writable属性,有的4个是

  1. get —— 一个没有参数的函数,在读取属性时工作,
  2. set —— 带有一个参数的函数,当属性被设置时调用,
  3. enumerable —— 与数据属性相同,
  4. configurable —— 与数据属性相同。

众所周知的协议,即以下划线"_"开头的属性是内部的,不应该从对象外部访问。eslint里没有 ,tslint有一种member-access的限制办法

1
2
Object.defineProperty(this, "age", {
get() {

对象有一个特殊的隐藏属性 Prototype 值是null或是另一个引用对象

1
2
3
4
5
6
7
8
9
10
11
12
let animal = {
eats: true
};
let rabbit = {
jumps: true
};

rabbit.__proto__ = animal; // (*)

// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**) 不存在属性 则从原型找,方法也是,原型也没有再一直向上直到找到或者遇到null
alert( rabbit.jumps ); // true

不能通过子的修改原型的数据属性,只会对子实体增加键,

而原型上的getters/setters,会运行,但是不论能找到的值在哪,只要解析this,始终是最初的子实体,也就是this不受到原型影响

意味着A有写方法W,会写this.X,B继承于A,那么B执行W会调用AW,但是写的this.x会写到B

按照c++的一些理解的话,是 他们之间有继承关系,所以执行函数会去找函数表,当子类没有时,就去父的表里找,找到执行时,传入的实体还是 子类

即使是这样,下面也列出了一个 错误/危险的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let hamster = {
stomach: [],

eat(food) {
this.stomach.push(food);
}
};

let speedy = {
__proto__: hamster
};

let lazy = {
__proto__: hamster
};

// This one found the food
speedy.eat("apple");
alert( speedy.stomach ); // apple

// This one also has it, why? fix please.
alert( lazy.stomach ); // apple

函数原型

设置 Rabbit.prototype = animal 的这段代码表达的意思是:“当 new Rabbit 创建时,把它的 [[Prototype]] 赋值为 animal” 。

1
2
3
4
5
6

function Rabbit() {}

/* 默认的 prototype
Rabbit.prototype = { constructor: Rabbit };
*/
1
2
3
4
5
6
7
function Rabbit() {}
Rabbit.prototype = { // 如果我们将整个默认原型替换掉,那么其中就不会有构造函数。
jumps: true
};

let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false

所以这个prototype,只是用于当一个 实例找不到对象key的时候,会去访问找个对象的原型(.prototype)的对应对象key,相当于用于记录的父类的一个属性,

__proto__(实例上原型上都有,指向它的原型的.prototype)和.prototype (一般在原型上)的关系

https://stackoverflow.com/questions/9959727/proto-vs-prototype-in-javascript

protovsprototype

1
2
( new Foo ).__proto__ === Foo.prototype;
( new Foo ).prototype === undefined;
  1. Object.create(proto[, descriptors]) —— 利用 proto 作为 [[Prototype]] 和可选的属性描述来创建一个空对象。
  2. Object.getPrototypeOf(obj) —— 返回 obj 对象的 [[Prototype]]。
  3. Object.setPrototypeOf(obj, proto) —— 将 obj 对象的 [[Prototype]] 设置为 proto。

潜在问题,如果我们的Object存的是用户提供的键值对,那么__proto__将会被忽略或其它错误,办法是使用Map,或者极简单的对象let obj = Object.create(null),它的prototype是null,缺点则是没有默认的toString方法

属性描述器定义出来的是不可枚举的

ES6的class语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function User(name) {
this.name = name;
}

User.prototype.sayHi = function() {
alert(this.name);
}

// ------------------------------
class User {

constructor(name) { // 必须与new 关键字一起使用
this.name = name;
} // 不需要逗号

sayHi() {
alert(this.name);
}
static staticMethod() { // 静态方法 等同于写 User.staticMethod = ....
alert(this === User);
}
}

// -------------------------------

let user = new User("John");
user.sayHi();

super.访问父类中的方法或些元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}

class Rabbit extends Animal {

constructor(name, earLength) { // 会报错,在 JavaScript 中,“派生类的构造函数”与所有其他的构造函数之间存在区别。在派生类中,相应的构造函数会被标记为特殊的内部属性 [[ConstructorKind]]:"derived"。
// 正确写法在这里调用 super(name)
this.speed = 0;
this.name = name;
this.earLength = earLength;
}

// ...
}

// 不生效!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.

不同点就在于:

当一个普通构造函数执行时,它会创建一个空对象作为 this 并继续执行。

但是当派生的构造函数执行时,它并不会做这件事。它期望父类的构造函数来完成这项工作。

A 是基于 B, B 基于 C,

在调用时候this不变,不会在原型链上找所有,只会调用原型链上的第一个,所以如果写法都是this,可能导致无限递归

[[HomeObject]] 特殊外部属性

这个绑定是永久的,内置的,会绑定到父类。因此这是一个非常重要的语言变化。

只有调用super.XXX时才会被使用,所以不会破坏兼容性,解决上面this不会变导致无限循环的bug

在这种情况下,类中的 func(){}写法和func:function(){}写法就不同了,后面一种冒号的写法不会绑定[[HomeObject]]

替换构造函数,

1
2
3
4
// 内置函数会使用它作为构造函数
static get [Symbol.species]() {
return Array;
}

最后的例子很有价值看一看,class X{} vs class X extends Object{}

  1. 用了extends的派生类 必须调用super
  2. 用了extends相当于设置了Rabbit.prototype.__proto__ = Object.prototype
  3. 用了extends相当于设置了Rabbit.__proto__ = Object);

如果没有extends的该处不会被设置,即无法访问静态方法Rabibit.getOwnPropertyNames()报错,其.__proto__ = Function.prototype

pic

xxx instanceof 某类检查一个变量实体是不是 属于一个类,改语法会在原型链上检测

该方法可以指定,设置 static [Symbol.hasInstance](obj){}自定义检测方法

如果没有[Symbol.hasInstance]方法,那么实际的比较过程就是

1
2
3
obj.__proto__ === Class.prototype
obj.__proto__.__proto__ === Class.prototype
obj.__proto__.__proto__.__proto__ === Class.prototype

实际的调用可以看做是类.prototype.isPrototypeOf(obj),可以看做这个调用,所以如果prototype被修改,即使__proto__没被改,rabbit instanceof Rabbit会返回false

也可以用toString来做类型识别,let arr=[];alert(arr.toString()),或者

objectToString = Object.prototype.toString;alert(objectToString.call(arr))

对于自动以class,可以写[Symbol.toStringTag]字段来改变toString()的输出

这样的话 就比typeof更强大(支持自定义),甚至可以带上更多自定义信息,替代instanceof

实例a 类名A Function Object
__proto__ 等于它来源类的prototype,如果是Object.create(实例b)来的,则指向实例b Object.__proto__ , 如果是 class extends B创建的 则等于B 见下 见下
prototype 它自己的prototype,相反的方向Foo.prototype.constructor === Foo 见下 Object的prototype
prototype.__proto__ Object.prototype Object.prototype null

Function.__proto__ === Function.prototype === Object.__proto__ !== Object.prototype

用于 返回
typeof 基本数据类型 string
{}.toString 基本数据类型、内置对象以及包含 Symbol.toStringTag 属性的对象 string
instanceof 任意对象 true/false

Mixin 模式 9.7

单继承模式有时会对人进行限制,所以有了mixin,能同时实现支持多个类的方法实现

根据维基百科的定义,mixin 是一个包含许多供其它类使用的方法的类,而且这个类不必是其它类的父类。

换句话说,一个 mixin 提供了许多实现具体行为的方法,但是我们不单独使用它,我们用它来将这些行为添加到其它类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// mixin
let sayHiMixin = {// mixin自身内部可以使用继承
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};

// 用法:
class User {
constructor(name) {
this.name = name;
}
}

// 拷贝方法
Object.assign(User.prototype, sayHiMixin);

// 现在 User 可以说 hi 了
new User("Dude").sayHi(); // Hello Dude!

User.mixinfunction()->首先通过User的prototype找到User.prototype,在调用mixinfunction时,因为mixin的类有自己的[[HomeObject]]所以如果有super,则会沿着mixin去找,而不是user

事件订阅mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let eventMixin = {
/**
* 订阅事件,用法:
* menu.on('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},

/**
* 取消订阅,用法:
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers && this._eventHandlers[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},

/**
* 触发事件并传递参数
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
return; // 对应事件名没有事件处理函数。
}

// 调用事件处理函数
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};

异常try…catch…finally 10.1

catch到的

name -> 异常名称

message -> 详情描述

stack -> 用于调试的

建议的:throw

1
2
3
4
5
let error = new Error(message);
// 或者
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

catch 应该只捕获已知的异常,而重新抛出其他的异常。

try -> catch -> finally。或者try -> finally

缺少catch时, try-finally->throw err

自定义错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}

class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}

当代码越来越大,对于每个层次应该有专门的处理和转换

Error 10.2

其实是warning,但是写warning 可能没人看

现代的 JavaScript 引擎会做很多优化。相对于「正常情况」,它们可能会改变「人为测试」的结果,特别是我们度量的目标很细微。因此,如果你想好好了解一下性能,请学习 JavaScript 引擎的工作原理。在那之后,你可能再也不需要微度量了。

关于 V8 引擎的大量文章,点击:http://mrale.ph.

Generator and Promise

我分别有写笔记

Module 简介

AMD — 最古老的模块系统之一,最初由 require.js 库实现。

CommonJS — 为 Node.js 服务器创建的模块系统。

UMD — 另外一个模块系统,建议作为通用的模块系统,它与 AMD 和 CommonJS 都兼容

<script type="module">和旧浏览器的nomodule

模块代码仅在第一次导入时被解析, 生成导出,然后它被分享给所有对其的导入,所以如果某个地方修改了 admin 对象,其他的模块也能看到这个修改。

Proxy

感谢

https://zh.javascript.info

有精力的朋友还是建议读上面链接,如果它们的文档有帮助,也建议购买它们的 epub/pdf 作为支持

本文是基于我C++的熟练,和一定时间js使用后的阅读知识补充整理,章节分化基本和上面链接的内容对应

该篇是对应上述链接的第二部分

Document 1

运行时 鸟瞰图

鸟瞰图

文档对象模型 DOM : document https://dom.spec.whatwg.org/

开发人员必须为每个浏览器编写不同的代码。那是昏暗、混乱的时代。

现在尽管有两个组在做document,但它们99%是相似的

当我们修改文档的样式规则时,CSSOM 与 DOM 一起使用。但实际上,很少需要 CSSOM,因为通常 CSS 规则是静态的。 https://www.w3.org/TR/cssom-1/

BOM(HTML规范的一部分):例如包括 https://html.spec.whatwg.org

navigator,提供有关浏览器和操作系统的背景信息。

location允许读取和重定向URL

Mozilla 手册 https://developer.mozilla.org/en-US/search

DOM:

#text永远是 DOM中的叶子节点

https://zh.javascript.info/dom-nodes

看这个举例,你会发现,比你预期的的#text会多更多,不过好在是这些都是#text,都是叶子节点,在过滤时只要规则正确,不会被过滤到

下面举例说如果你把各个<html><body>等之间的空格回车都去掉,那它们生成的dom树就不再会有多余的#text

开发者工具中的 DOM 结构已经过简化。文本节点仅以文本形式显。根本没有“空白”(只有空格)的文本节点

历史原因:

  1. 由于历史原因,<head> 之前的空格和换行符被忽略,
  2. 如果我们在 </body> 之后放置了一些东西,那么它会自动移动到 body 内部,因为 HTML 规范要求所有内容必须位于 <body> 内。所以 </body> 后面可能没有空格。

自动修正【看看,更多的还是应该保证 正确的编写

表格是一个有趣的“特例”。按照 DOM 规范,它们必须具有 <tbody>,但 HTML 文本可能(官方的)忽略它。然后浏览器自动在 DOM 中创建 <tbody>

有12个节点类型,常用的就是你F12能看到的document,元素节点,文本节点,和注释

http://software.hixie.ch/utilities/js/live-dom-viewer/

以前也没系统的看过chrome的控制台方法 官方文档 TODO: https://developers.google.com/web/tools/chrome-devtools/

现在最后选中的元素可以用 $0 来进行操作,以前选择的是 $1,如此等等。

我们可以在它们之上运行命令。例如,$0.style.background = ‘red’ 使选定的列表项变成红色,

inspect(node)来从命令行,反过去找document中的元素

DOM 结构

DOM

document.body,document.head

在DOM中用===null判断节点不存在

childNodes一个类似数组的个迭代集合只读,引用(实时的), ===.firstChild, ===.lastChild,.hasChildNodes()

.previousSibling,.nextSibling

如果希望忽略文本和注释节点,只访问元素节点

DOM

table支持更多的东西[感觉这部分主要是了解,因为 实际编写中,现在主流框架,基本很少需要进行原生元素的操作了[github说没用任何框架除外

.getElementsBy*:Id,TagName,Name,ClassName

.querySelectorAll() ,querySelector,.matches,.closest(最近祖先匹配) 都是css相关的调用 方法

注意getElementsBy*返回的是 一个实时会变动的集合,而querySelectorAll返回的是一个static

场景:

  1. 获取元素

  2. 改变元素

  3. 操作元素 [如果用的querySelectorAll得到的,那么可能和当前真是的元素不一致]

原文上给了几个操作的一个总结表[感觉因为现在基本都是框架就不整理进来了]

DOM节点类

Node

Node 是实现了DOM树上的节点方法,父,子,前后节点等

Element 是DOM元素的基类,在Node之上,提供了.*Element.*,query*

.nodeType == 检查,是不是,元素节点,文本节点,document对象

.nodeName(Node定义) 和.tagName(Element的节点) 总是全大写

.innerHTML字符串html,.outerHTML , 相对于.innerHTML会多一层外层的包裹,注意到这些都是指针式的操作,所以outerHTML改完以后,不应该继续使用

.data可以获取.nodeValue/data文本节点内容

.textContent纯文本 [安全的!也就是不担心元素注入]

.hidden是否可见 等同于display:none;

除此以外,特例

input,select,textarea都有.value

ahref

id

事件简介 2

UI事件 3

表单,控件 4

加载文档和其他资源 5

杂项 6


MORE

Task/MicroTask

https://javascript.info/event-loop

同步

Task: script标签加载完,一个用户事件如Click,一个setTimeout

MicroTask: MutationObserver callbacks,例如promise的callback 或 await以后

看规范 https://tc39.es/ecma262/#sec-jobs

Host environments are not required to treat Jobs uniformly with respect to scheduling. For example, web browsers and Node.js treat Promise-handling Jobs as a higher priority than other work; future features may add Jobs that are not treated at such a high priority.

其实只严格FIFO