principle

变量提升

1、函数声明可以提升

1
2
3
4
foo() //  3
function foo() {
console.log(3)
}

2、变量可以提升,但是赋值不能提升

1
2
3
4
fooNext() // Uncaught TypeError: fooNext is not a function
var fooNext = function () {
console.log(2)
}

直接打印d会报错,但是打印下面声明了d则提示undefined

1
2
3
4
5
console.log(d) // Uncaught ReferenceError: d is not defined

// =========
console.log(d) // undefined
var d

3、提升示例1

这个结果并不是2,首先在函数obj内,是可以访问到全局变量o的。但是由于函数内部声明了一个局部的同名变量o,由于变量被提升到执行环境顶端,覆盖掉了全局的声明。

1
2
3
4
5
6
7
8
9
10
11
12
var o = 2;
function obj() {
console.log(o)
var o;
}
obj(); // undefined

// 由于声明提升,可以简单理解为等价于下面这样,内部声明的o覆盖掉了全局的o
function obj() {
var o;
console.log(o)
}

4、提升示例2

这里有两个坑,一是块级作用域,二是变量提升,首先{}没有块级作用域,所以a被提升到全局环境最前面,所以a in window 是true,取反后为false所以不执行var赋值语句。

1
2
3
4
5
if (!('foo' in window)) {
var foo = 1;
}

console.log(foo); // undefined

5、提升示例3

函数声明会覆盖变量声明,但不会覆盖变量赋值

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log(2);
}
var foo;
console.log(foo); // foo(){console.log(2);}


function foo() {
console.log(2);
}
var foo = 3;
console.log(foo); // 3

js应用环境

  • 宿主环境, 壳程序(shell)和web浏览器

  • 运行环境是js引擎内建的

js调用栈

一个静态的,为调用的函数只是一个值,一但调用,系统将当前函数入栈,并保留函数的执行指针.执行完后,出栈,并继续执行指针后的代码

callee

arguments对象拥有一个callee,该成员总指向该参数arguments的创建函数.

函数递归和匿名递归都可以写成:

1
2
3
void funtion () {
arguments.callee()
}

Proxy 和 Object.defineProperty

Object.defineProperty 描述符可拥有的键值

configurable enumerable value writable get set

数据描述符| 可以| 可以| 可以| 可以| 不可以| 不可以
存取描述符 |可以 |可以 |不可以| 不可以| 可以| 可以

  • 如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符

  • 如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常

缺点

  • Object.defineProperty的第一个缺陷,无法监听数组变化

  • Object.defineProperty的第二个缺陷,只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历

优点

  • 兼容性好

Proxy

优点

  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的

  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改

  • Proxy 支持数组拦截

缺点

  • 当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写

Reflect 作用

  1. 将Object对象的一些属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上

  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

  3. 让Object操作都变成函数行为。比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为

  4. 让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础

load 和 DOMContentLoaded

DOMContentLoaded:仅当DOM加载完成,不包括样式表,图片(async加载脚本不一定完成)

onload:页面上所有的DOM,样式表,脚本,图片都已经加载完成了

函数编程

重点: 在内部保存数据和对外无副作用, 由闭包实现

闭包两个特性:

  • 在函数执行过程中,处于激活状态,可访问

  • 在函数运行结束后,保持运行过程中最终数据状态.

函数调用方式

  • 方法调用,此时this指向该对象

  • 函数调用,此时this指向window

  • 构造器调用, 通过函数前加new调用,将创建一个隐藏链接到该函数的prototype原型对象的新实例对象,同时this会指向新对象.

  • apply和call动态调用

函数调用和引用本质

函数被调用之前,没有实际价值,在预编译时,也只是简单分析词法,语法结构,并根据标识符预定一个函数占据内存,内部逻辑并没有执行,一旦函数执行会立即产生上下文,是一个动态环境,动态的概念.

引用:当引用函数时,多个变量内存储的是相同的入口指针,指向同一块内存.

调用:执行函数,并返回函数的返回值.

闭包

函数和对其周围状态(词法环境)的引用捆绑在一起构成闭包。也就是说,闭包可以让你从内部函数访问外部函数作用域

闭包两个用途

  • 内部函数访问外部函数作用域

  • 让这些变量的值始终保持在内存中

注意事项

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包

  • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时不要随便改变父函数内部变量的值

在循环中创建闭包:一个常见错误

  • 下面代码你会发现没有达到预期,返回 这是0 这是1 这是2,结果都是 这是2
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
let queue = [];
function setupHelp() {
var obj = {
"0": "这是0",
"1": "这是1",
"2": "这是2"
};
for (var i = 0; i < 3; i++) {
var text = obj[i];
queue.push(function () {
console.log(text);
});

// 解决问题的代码使用更多闭包
// function out(a) {
// return function aa() {
// console.log(a);
// };
// }
// queue.push(out(text));
}
}

setupHelp();

while (queue.length) {
queue.shift()();
}
  1. 闭包是由他们的函数(aa)定义和在 setupHelp 作用域中捕获的环境所组成的

  2. 这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量text

  3. 变量 text 使用var进行声明,由于变量提升,所以具有函数作用域

  4. 当queue.shift()()执行时,由于循环在事件触发之前早已执行完毕,变量text(被三个闭包所共享)已经是最后一个值

惰性和非惰性

非惰性:不管表达式是否被利用,只要在执行代码中,都会被计算

1
2
3
4
5
6
7
var a = 2

function f (x) {
return x
}
console.log(f(a, a=a*a))
console.log(f(a))

惰性: 对函数或请求的处理延迟到真正需要结果时在进行处理。它的目的是要最小化计算机要做的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 每次调用f都会重新求值
var t;
function f() {
t = t ? t : new Date();
return t;
}
f()

改进:

var f = function () {
console.log(33) // 只执行一次
var t = new Date() // 只执行一次
f = function () {
return t;
}
return f();
}
console.log(f())
console.log(f())
console.log(f())
console.log(f())
console.log(f())

weakMap

  1. WeakMap 只接受对象作为键名
1
2
3
const map = new WeakMap();
map.set('key', 2); // TypeError: Invalid value used as weak map key
map.set(null, 2); // TypeError: Invalid value used as weak map key
  1. WeakMap 的键名所引用的对象是弱引用
1
2
3
4
5
6
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];

上面代码,一旦不再需要e1和e2这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1和e2占用的内存,造成内存泄露

1
2
arr [0] = null;
arr [1] = null;

WeakMap 则很好的解决了这个问题,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap

1
2
3
4
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element)

论证代码

垃圾回收机制

JS引擎中对变量的存储主要有两种位置,栈内存和堆内存

  • 栈内存存储基本类型数据以及引用类型数据的内存地址

  • 堆内存储存引用类型的数据

标记清除采用的收集策略

  • JavaScript中的垃圾收集器运行时会给存储在内存中的所有变量都加上标记

  • 去掉环境中的变量以及被环境中的变量引用的变量的标记

  • 剩余有标记的变量被视为准备删除的变量

  • 垃圾收集器完成内存清除,销毁并回收其占用的内存

引用计数(不怎么用了)

跟踪记录每个值被引用的次数

  • 当声明一个变量并将一个引用类型值赋值给该变量时,这个值的引用次数为1

  • 若同一个值(变量)又被赋值给另一个变量,则该值的引用次数加1

  • 但是如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1

  • 当这个值的引用次数为0时,则无法再访问这个值,就可回收其占用的内存空间

避免如下的循环引用

1
2
3
4
5
const a = {key: 1}
const b = {key: 2}

a.point = b
b.point = a

v8垃圾回收

JS 面向对象

  • 面向对象语言的三大特征: 封装、继承、多态

  • 各自特点:

封装: 是隐藏逻辑实现过程,只对外暴露属性和方法,提高安全性和复用性;

继承: 建立对象之间的父子关系,使子对象拥有父级对象的属性和方法,提高复用性;

多态: 在同一个方法中,由于参数不同而导致执行效果各异,便于拓展;

  • JS中函数继承主要继承的是函数名,不是函数体,函数本身是引用类型,通过构造方法生产新的对象时,只是指针的存储变动而不是函数体

  • 所有字符串都是String的实例,数字都是Number的实例,字符串和数字本身没有方法,都是通过原型链分别找到了String和Number的方法

typeof

1
2
3
4
5
6
7
8
9
10
11
12
13
typeof (() => {})             // function
typeof [] // object
typeof {} // object
typeof null // object
typeof undefined // undefined
typeof (new Promise(() => {}))// object
typeof Promise // function
typeof Symbol('3') // symbol
typeof 3 // number
typeof true // boolean
typeof '' // string
'子君' instanceof String // false
new Date() instanceof Date // true

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上 object instanceof constructor

instanceof,无法判断基本类型,但可以正确判断引用类型

对象的toString

1
2
3
4
5
6
7
const d = {}
const e = {key: '1'}
const f = {key: '2'}
d[e] = '12'
d[f] = '21'

console.log(d[e]) // 21

因为e和f都是对象,而对象的key只能是数值或字符,所以会将对象转换为字符,对象的toString方法返回的是[object Object]

基本包装类型

为了便于操作基本类型值,ECMAScript 提供了 3 个特殊的引用类型:Boolean、Number 和String,每当读取一个基本类型值得时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据

1
2
var s1="some text";  
var s2=s1.substring(2);

s1 是基本类型值,而s1代码调用了 substring() 方法,我们知道,基本类型值不是对象,因而从逻辑上讲它们不应该有方法。

为了让我们实现这种直观的操作,后台已经自动完成了一系列的处理。当第二行代码访问 s1 时,访问过程处于一种读取模式,也就是从内存中读取这个字符串的值。而在读取模式中访问字符串时,后台都会自动完成下列处理。

  • 创建 String 类型的一个实例;

  • 在实例上调用指定的方法;

  • 销毁这个实例;

类比过程如下

1
2
3
4
5
var s1=new String("some text");  

var s2=s1.substring(2);

s1=null;

引用类型与基本包装类型的主要区别就是对象的生存期

浏览器是多进程, js 是单线程

浏览器是多进程的,每一个tab页代表一个进程,如果多个空的tab页会合为一个进程(打开chrome任务管理器)

每个进程有多个线程:

  • GUI 渲染线程:将HTML构造为DOM树,将CSS构造为CSSOM树,DOM树与CSSOM树合并为render树,渲染为布局,最后绘制为浏览器页面,界面需要重绘或重排,该线程就会执行

  • JS 引擎线程:解析执行javascript脚本,JS 引擎线程 和 GUI 渲染线程是互斥的,JS引擎执行时间过长,会阻塞渲染引擎

  • 事件触发线程:JS引擎在执行javascript代码时,会形成相应的执行栈,执行同步任务,异步任务的事件回调扔到任务队列。等待同步任务执行完毕,执行任务队列中的任务(宏任务,微任务)

  • 定时器触发线程:开启定时器时,触发定时器线程,定时器结束后,事件回调函数添加到任务队列,等待JS引擎处理

  • http 请求线程:当 http 请求时会开启一条请求线程,当请求完毕时,将请求的回调函数添加到任务队列,等待JS引擎处理

设计模式原则

单一职责原则:

  • 一个程序只做好一件事

  • 如果功能过于复杂就拆分开,每个部分保持独立

开放封闭原则:

  • 对扩展开放,对修改封闭

  • 增加需求时,扩展新代码,而非修改已有代码

里氏替换原则:

  • 子类能覆盖父类

  • 父类能出现的地方子类就能出现

接口隔离原则:

  • 保持接口的单一独立

  • 类似单一职责原则,这里更关注接口

依赖倒转原则:

  • 面向接口编程,依赖于抽象而不依赖于具体

  • 使用方只关注接口而不关注具体类的实现

设计模式

工厂模式

工厂模式是为了解决多个类似对象声明的问题;也就是为了解决实列化对象产生重复的问题。

将其成员对象的实列化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型。

父类只对创建过程中的一般性问题进行处理,这些处理会被子类继承,子类之间是相互独立的,具体的业务逻辑会放在子类中进行编写。

第一:弱化对象间的耦合,防止代码的重复。在一个方法中进行类的实例化,可以消除重复性的代码。

第二:重复性的代码可以放在父类去编写,子类继承于父类的所有成员属性和方法,子类只专注于实现自己的业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Product {
constructor(name) {
this.name = name
}
common() {
console.log('common')
}
}

class Factory {
create(name) {
return new Product(name)
}
}

let factory = new Factory()
let p = factory.create('p1')
p.common()

观察者模式

  • 定义了一种一对多的关系

  • 让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新

使用场景

当一个对象的改变需要同时改变其它对象,且不知道具体有多少对象需要改变

实例:vue源码解析 和 Promise 源码解析

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
41
class Subject {
constructor() {
this.state = 0
this.observersQueue = []
}
getState() {
return this.state
}
setState(state) {
this.state = state
this.notify()
}
notify() {
this.observersQueue.forEach(observer => {
observer.update()
})
}
add(observer) {
this.observersQueue.push(observer)
}
}

// 观察者
class Observer {
constructor(name, subject) {
this.name = name
this.subject = subject
this.subject.add(this)
}
update() {
console.log(`${this.name} update`)
}
}

// 测试
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('02', s)

// 修改数据后通知更新
s.setState(12)

优点

  • 支持简单的广播通信,自动通知所有已经订阅过的对象

  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用

  • 松散耦合

缺点

过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

发布订阅模式

  • 在设计模式结构上,发布订阅模式继承自观察者模式,是观察者模式的一种实现的变体,在设计模式意图上,两者关注点不同,一个关心数据源,一个关心的是事件消息,在观察者模式中增加了中间事件管理层

  • 数据源告诉第三方(事件频道)发生了改变,第三方再通知订阅者发生了改变

  • 在发布订阅模式里,发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识

  • 发布订阅模式里,发布者和订阅者,不是松耦合,而是完全解耦的

vue 和 react

Vue三要素

响应式: 劫持数据,监听数据变化,其中的实现方法就是我们提到的双向绑定

模板引擎: 如何解析模板

渲染: Vue如何将监听到的数据变化和解析后的HTML进行渲染

基于数据劫持的双向绑定, 目前Vue在用的Object.defineProperty,另一个是ES2015中新增的Proxy,而Vue的作者宣称将在Vue3.0版本后加入Proxy从而代替Object.defineProperty

MVVM 数据双向绑定:数据变化更新视图,视图变化更新数据

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化

  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化

数据劫持的优势

目前业界分为两个大的流派,一个是以React为首的单向数据绑定,另一个是以Angular、Vue为主的双向数据绑定

数据劫持的优势所在

无需显示调用: 例如Vue运用数据劫持+发布订阅,直接可以通知变化并驱动视图,

可精确得知变化数据:我们劫持了属性的setter,当属性值改变,我们可以精确获知变化的内容newVal,因此不需要额外的diff操作, 如果只知道变化不知道变化在哪里,需要大量的diff比较。

数据劫持双向绑定的实现思路

  • 利用Proxy或Object.defineProperty生成的Observer针对对象/对象的属性进行”劫持”,在属性发生变化后通知订阅者

  • 解析器Compile解析模板中的Directive(指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染

  • Watcher属于Observer和Compile桥梁,它将接收到的Observer产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化

返回
顶部