JS 进阶

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
(() => {
let x, y
try {
throw new Error()
} catch (x) {
// x 为 catch 变量 y 为全局声明变量
(x = 1), (y = 2)
console.log(x) // 1
}
console.log(x) // undefined
console.log(y) // 2
})()

catch 代码块接收参数 x。当我们传递参数时,这与之前定义的变量 x 不同。这个 x 是属于 catch 块级作用域的。

然后,我们将块级作用域中的变量赋值为 1,同时也设置了变量 y 的值。现在,我们打印块级作用域中的变量 x,值为 1

catch 块之外的变量 x 的值仍为 undefinedy 的值为 2。当我们在 catch 块之外执行 console.log(x) 时,返回 undefinedy 返回 2

1
2
3
4
5
let person = { name: "Lydia" };
const members = [person];
person = null;

console.log(members); // { name: "Lydia" }

首先我们声明了一个拥有name属性的对象 person

img

然后我们又声明了一个变量members. 将首个元素赋值为变量person。当设置两个对象彼此相等时,它们会通过 引用 进行交互。但是当你将引用从一个变量分配至另一个变量时,其实只是执行了一个 复制 操作。(注意一点,他们的引用 并不相同!)

img

接下来我们让person等于null

img

我们没有修改数组第一个元素的值,而只是修改了变量person的值,因为元素(复制而来)的引用与person不同。members的第一个元素仍然保持着对原始对象的引用。当我们输出members数组时,第一个元素会将引用的对象打印出来。

1
const num = parseInt("7*6", 10) // 7

只返回了字符串中第一个字母。设定了 进制 后 (也就是第二个参数,指定需要解析的数字是什么进制:十进制、十六机制、八进制、二进制等等……),parseInt 检查字符串中的字符是否合法。一旦遇到一个在指定进制中不合法的字符后,立即停止解析并且忽略后面所有的字符。

*就是不合法的数字字符。所以只解析到"7",并将其解析为十进制的7. num的值即为7.

1
2
3
4
5
const name = "Lydia";
age = 21;

console.log(delete name); // false
console.log(delete age); // true

delete操作符返回一个布尔值:true指删除成功,否则返回false. 但是通过 var, constlet 关键字声明的变量无法用 delete 操作符来删除。

name变量由const关键字声明,所以删除不成功:返回 false. 而我们设定age等于21时,我们实际上添加了一个名为age的属性给全局对象。对象中的属性是可以删除的,全局对象也是如此,所以delete age返回true.

函数柯里化(Curry):函数元(参数)降维技术

1
2
3
4
5
6
7
8
9
10
11
const a = (b, c) => b + c

//柯里化

const a1 = b => c => b + c // a === a1

const curryAdd = curry((a, b, c) => a + b + c);

curryAdd(1)(2)(3); // 6


1
2
3
4
5
6
7
8
9
10
11
12
13
const curry = function(fn){ // bind(绑定) fn
let length = fu.length; // bind length
let params = []; // bind params
return function partial(x){
params.push(x)
if(params.length == length){ // 递归终止条件
return fu(...params); // use fn
}else{
return partial; // 返回函数 递归传参
}
}

}
1
2
3
[y] = [1, 2, 3, 4, 5];
// y === 1
// 解构赋值

img

1
2
3
4
5
6
7
8
9
10
11
12
13
// module.js 
export default () => "Hello world"
export const name = "Lydia"

// index.js
import * as data from "./module"

console.log(data)
// 输出
{
default: function default(),
name: "Lydia",
}

构造函数语法糖 如果用 构造函数 的方式重写 person 类则是:

1
2
3
4
5
6
7
8
9
10
11
// 类
class Preson{
constructor(name) {
this.name = name
}
}

// 构造函数的方式重写
function Preson(name) {
this.name = name
}
1
2
3
4
5
6
7
8
9
10
11
const myMap = new Map()
const myFunc = () => 'greeting'

myMap.set(myFunc, 'Hello world!')

//1
myMap.get('greeting')
//2
myMap.get(myFunc)
//3
myMap.get(() => 'greeting')

当通过 set 方法添加一个键值对,一个传递给 set方法的参数将会是键名,第二个参数将会是值。在这个 case 里,键名为 函数 () => 'greeting',值为'Hello world'myMap 现在就是 { () => 'greeting' => 'Hello world!' }

1 是错的,因为键名不是 'greeting' 而是 () => 'greeting'。 3 是错的,因为我们给get 方法传递了一个新的函数。对象受 引用 影响。函数也是对象,因此两个函数严格上并不等价,尽管他们相同:他们有两个不同的内存引用地址

创建一个新的变量 counterTwo 并将 counOne 的 引用地址 赋值给他(Two、One 指向内存同一个地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Counter {
constructor() {
this.count = 0;
}

increment() {
this.count++;
}
}

const counterOne = new Counter();
counterOne.increment();
counterOne.increment();

const counterTwo = counterOne; // 注意看内存图解 (下左)
counterTwo.increment(); // 下右

console.log(counterOne.count); // 3

img

事件循环机制**event loop**

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
const myPromise = Promise.resolve("Promise!");
function funcOne() {
// 加入微任务队列
myPromise.then(res => res).then(res => console.log(res+'1'));
myPromise.then(res => res).then(res => console.log(res+'2'));
myPromise.then(res => res).then(res => console.log(res+'3'));
// 加入宏任务队列
setTimeout(() => console.log("Timeout1!"), 0);
console.log("Last line1!");
}
async function funcTwo() {
console.log('two');
// 第一个 await 阻塞代码 先执行
const res = await myPromise;
// 继续执行script宏任务里的 同步代码
console.log(res+0);
// 第二个 await 阻塞代码 加入微任务队列,开始执行微任务队列
console.log(await res+4);
// 加入宏任务队列
setTimeout(() => console.log("Timeout2!"), 0);
console.log("Last line2!");
// 第三个 await 阻塞代码 同第二个await
console.log(await res+5);
console.log("Last line3!");
}
funcOne();
funcTwo();
// Last line1! two
// Promise!0(第一个 await 开始执行微任务队列任务,阻塞后面代码,先执行) 微任务队列
// Promise!1 Promise!2 Promise!3
// Promise!4 微任务队列
// Last line2!
// Promise!5 微任务队列
// Last line3!
// Timeout1 Timeout2 宏任务队列

疑难杂症

对象的键被自动转化为字符串,当对象化为字符串时,变成”[object object]”

导致事件的最深嵌套的元素是事件的 target 可以通过event.stopPropagation 来阻止事件冒泡

.apply .call .bind (parameter1, parameter2)

parameter1 传递对象引用

parameter2 所传递对象的参数(apply是数组,而call是参数列表,且为一次性传入,bind可多次动态传入,参数列表形式传入)

bind 返回函数的副本,但带有绑定上下文!它不是立即执行的

apply call 立即执行

7 种内置类型

string number (bigint) boolean null undefined symbol object

function 不是一种类型 属于 object 对象

JavaScript 中的一切都是 基本类型(7)+ 对象

string 是可迭代的 […’Lydia’] = [‘L’, ‘y’, ‘d’, ‘i’, ‘a’]

引入的模块 是只读

1
import myCounter from "./counter"; // myCounter 为只读属性

引入的模块是 只读 的:你不能修改引入的模块。只有导出他们的模块才能修改其值。

JavaScript中闭包(Closure,上面我们已经提到过)产生的原因,一个函数还没有被销毁(调用没有完全结束),你可以在子环境内使用父环境的变量。

delete 不能删除 const let var 声明的变量 返回 false

defineProperty(object, key, { value: 1 }) 给指定对象添加 key属性 不可枚举

import 命令是编译阶段执行的,在代码运行之前,被导入的模块代码,先运行。区别于 CommonJsrequirerequire 按需加载,执行顺序取决于 require 所在代码位置

functong * name(params){} : 表示声明了 一个函数生成器Genarator 对象,通常 name 生成器函数,配合yeild 使用,以达到 name 函数内部逻辑 暂停 恢复执行name.next() 得到一个 { value: 值, done: true } 对象。next 方法可以带参数 name.next('param1')param1 将传递给 name 函数的 上一个 yeild 表达式的返回值。注:yeild 只能在生成函数中使用。

Object.freeze(obj) 使得obj对象的属性 无法添加、修改、删除(冻结对象)

尝试打印一个未定义的对象,报错 ReferenceError 引用错误

数组元素:任何值、数学表达式、日期、函数计算

定时器 setTimeout setInterval 中,this 指向 全局对象 windows

function(a, b = a) 可以将函数参数的默认值 设置为另一个参数,只需另一个参数定义于这个函数参数之前

push 方法返回 数组长度 [1,2,3].push(5) === 4

function(a, b, ...c){} ...c 表示 剩余参数 是一个 数组 只能 作为最后一个参数 否则抛出 语法错误 syntaxError

Javascript 引擎自动在语句后添加 ;

可以将设置为等同于其他类/函数 构造函数

symbol 类型是不可 枚举的 Object.keys(obj) 将不可见 symbol 类型的 keyObject.getOwnPropertySymbols(obj) 方法访问

箭头函数 若返回 一个值,则 () => 不必要写 {} ,若返回 对象 则必须 () => { return { obj } }() => ({ obj })

"string"() 字符串调用(非函数不能调用)报错 TypeError 类型错误

常见 假值 null undefined "" 0 -0 NaN false 0n

微任务promise.then() process.nextTick() MutationOvserver() 宏任务setTimeout setInterval script setImmediate I/O UI-rendering await 执行当前代码,将后续代码加入 微任务队列。等价于 先执行new Promise()后,将 promise.then() 加入微任务队列 事件循环数据结构微任务队列宏任务队列不断循环执行 宏任务队列

a + b 若 a、b 不全为 数字 Js引擎 会将 a、强制转为 字符串 { name: value } 对象强转字符串为 [ Object object ] ,再进行字符串拼接

对象通过引用传递(引用内存的位置),当我们检查对象的严格性相等时=== ,我们正在比较他的引用

. 表示法访问对象属性,Obj.name,会使用改确切名称在对象上查找属性,若没有name ,则返回 undefined

Obj['name'],方括号表示法,它会从第一个 [ 开始直到找到右方括号 ],之后才开始评估 语句,[]中可以插入表达式,运行时才确认的属性,而. 属性不能访问。

❤️ 表情符号是 unicode,对于相同的 表情符号,他们总是相等的 如❤️ === ❤️

map slice filter 返回一个新数组,find返回一个元素,reduce返回一个计算 值

在 javascript 中 原始类型通过 起作用。当 原始类型 如字符串 的值是数组的值时,该字符串的值只是 数组引用值的复制,改变字符串值时,数组值并不会被改变

let constvar 声明的变量都会有 提升,但与var 不同的是,它们不会被初始化 undefined.在声明之前不能访问它,这个行为成为暂时性死区,试图访问会抛出ReferenceError (暂时性死区:在 let 声明之前的执行瞬间)

((x => x)('param')) 是个立即执行函数 相当于向箭头函数 x => x 传递参数 param 并且执行

() => 箭头函数并没有属于自己的 thisthis捕获其所在上下文的this,作为自己的值

??= 逻辑空赋值,x ??= value 仅在 x = 空值(undefined 或 null)时,对其赋值 value

get语法将对象属性绑定到查询该属性时被调用的方法。(当 访问 obj.name时,就会调用 get name()方法若有)。当尝试设置属性时,set语法将对象属性绑定到要调用的函数,它还可以在类中应用。(当 修改属性 obj.name = value时,就会调用 set name(value)方法若有)。get、set方法返回 undefined

!typeof name === "string" : 先 计算 typeof name 再取反 ! 最后再检验 ===

Number.isNaN(value) 检测 value是否是 数字且等价于NaN isNaN 检测 value 是不是 不是一个数字

Object.seal(obj) 可以防止新属性被添加、存在属性 被移除。然而、仍然可以对存在属性 进行更改

Object.freeze(obj)对 对象进行浅冻结 ,不能对 obj 的属性 添加、修改、删除。但可以操作 obj 里的对象属性(浅冻结)

ES2015 -> es6 ... ES2020 -> es11,在 ES2020 中,通过 #可以给 class添加私有变量,但在 class 外部我们无法获取该值,尝试获取、则会报错 syntaxError 语法错误

对象默认不是可迭代的,可以通过添加对象迭代器[sysmbol.iterator]来定义迭代规则,其返回一个generator函数*[sysmbol.iterator](){}[...obj]无法解构,{...obj}可以解构

classconstructor中的属性,不会在原型链上共享

Promise.all方法可以并行运行promise,如果其中一个promise失败了,Promise.all方法会带上rejectpromise的值_rejects_

函数式编程特性

说了这么久,都是在讲函数,那么究竟什么是函数式编程呢?在网上你可以看到很多定义,但大都离不开这些特性。

  • First Class 函数:函数可以被应用,也可以被当作数据。
  • Pure 纯函数,无副作用:任意时刻以相同参数调用函数任意次数得到的结果都一样。
  • Referential Transparency 引用透明:可以被表达式替代。
  • Expression 基于表达式:表达式可以被计算,促进数据流动,状态声明就像是一个暂停,好像数据到这里就会停滞了一下。
  • Immutable 不可变性:参数不可被修改、变量不可被修改—宁可牺牲性能,也要产生新的数据(Rust内存模型例外)。
  • High Order Function 大量使用高阶函数:变量存储、闭包应用、函数高度可组合。
  • Curry 柯里化:对函数进行降维,方便进行组合。
  • Composition 函数组合:将多个单函数进行组合,像流水线一样工作。

另外还有一些特性,有的会提到,有的一笔带过,但实际也是一个特性(以Haskell为例)。

  • Type Inference 类型推导:如果无法确定数据的类型,那函数怎么去组合?(常见,但非必需)
  • Lazy Evaluation 惰性求值:函数天然就是一个执行环境,惰性求值是很自然的选择。
  • Side Effect IO:一种类型,用于处理副作用。一个不能执行打印文字、修改文件等操作的程序,是没有意义的,总要有位置处理副作用。(边缘)

数学上,我们定义函数为集合A到集合B的映射。在函数式编程中,我们也是这么认为的。函数就是把数据从某种形态映射到另一种形态。注意理解“映射”,后面我们还会讲到。

浏览器性能优化

渲染流程

渲染进程 html -> DOM树

渲染引擎 css -> css样式树 计算dom节点样式

构建布局树 Dom树 + css样式树

生成图层树 对布局树进行分层

每个图层生成绘制列表 提交给合成线程

每个图层单独绘制

合成图层

渲染性能

  1. 减少回流(重排:大小、位置、布局几何信息发生变化)、重绘 (颜色、背景、border)

  2. 将回流、重绘的元素作为单独的图层(css3D、canvas、position: fixed、video、css3动画节点)

    浏览器以 图层 为单位 进行渲染

  3. 元素作为单独图层(will-change: transform ),使用 opacity 或者 transform: transformX(100px) 不会触发 回流、重绘(元素偏移量右 100px 则会交给GPU处理)