Joe's Website


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

如何实现一个 better than Promises/A+ 规范的 promise 底层类库

发表于 2020-08-12 | 分类于 JavaScript

背景

提起Promise,大家并不陌生。Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件 )更合理和更强大。它由前端社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。然而对于 IE,无论是 IE6-10,还是最新版本的 IE11,都不支持 Promise,或许MS IE认为前端程序员可以自己轻松去实现这个标准,又或许不认同已形成统一的规范,认为它可能存在一些缺点。但是无论怎样,Promise的出现给前端带来了里程碑式的变化,解决了回调地域的问题,进而 async await的出现让前端代码前所未有的优雅。但是,如果要支持 IE(起码要支持最新版的 IE11),我们该如何做?当然我们可以去使用别人写好的 pollyfill,但是不无前车之鉴,别人的代码不可能是永久安全的,更何况如此核心的一个 API,可能由于信息安全等方面的因素我们不能去随意引用别人的类库。所以本文将阐述如何去实现一个遵循 Promises/A+规范的 Promise 类库,不仅做好了对 IE 的支持,又在理解 Promise 原理的同时,提升了自身的编程思想和能力。

前置

Promises/A+,建议通篇阅读,Promises/A+是在Promises/A的基础上对原有规范进行修正和增强。Promises/A+组织会因新发现的问题以向后兼容的方式修改规范,且每次修改均会经过严格的考虑、讨论和测试,因此Promises/A+规范相对来说还是比较稳定的。

实现

  1. 首先 Promise 是一个类,我们可以通过 new 来创建 Promise 实例。参数是 executor,顾名思义,这是一个执行器且会立即执行。
  2. executor 有两个参数 resolve 和 reject,resolve 代表成功的回调,reject 代表失败的回调。
  3. Promise 默认有三个状态: 等待,成功,失败,默认为等待。调用 resolve 变为成功,调用 reject 变为失败。
  4. 返回的实例上有一个 then 方法。then 中有两个参数:成功对应的函数和失败对应的函数。
  5. 如果同时调用成功和失败,默认会采用第一次调用的结果。
  6. 抛出错误会走失败逻辑。
  7. 成功时可以传入成功的值(value),失败时可以传入失败的原因(reason)。
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
// 枚举三种状态
const ENUM_STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}

class Promise {
constructor (executor) {
this.status = ENUM_STATUS.PENDING
this.value = undefined
this.reason = undefined

const resolve = value => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value
}
}
const reject = reason => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.REJECTED
this.reason = reason
}
}
try {
executor(resolve, reject) // 立即执行
} catch (error) {
reject(error)
}
}
then (onFulfilled, onRejected) {
if (this.status === ENUM_STATUS.FULFILLED) {
onFulfilled(this.value)
}
if (this.status === ENUM_STATUS.REJECTED) {
onRejected(this.reason)
}
}
}

这就结束了吗?不,以上才刚刚开始,实现了一个“乞丐版”的 Promise。仔细看我们会发现如下问题:
调用 then 方法时,如果加入了定时器,会存在很大问题:因为没有考虑到异步的情况,调用 then 时并没有拿到成功或者失败的状态,所以会一直处于 pending 状态。多次调用 then 时存在异步又如何处理?所以,当我们调用 then 时如果是 pending 状态,则需要把成功和失败的回调分别进行保存。调用 resolve 时,将保存的函数进行执行。这是一个发布-订阅模式,可以加深理解,具体实现如下:

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
42
43
44
45
46
47
48
49
50
51
52
class Promise {
constructor (executor) {
this.status = ENUM_STATUS.PENDING
this.value = undefined
this.reason = undefined

// 创建成功和失败的队列用于分别存放
this.onResolvedCallbacks = []
this.onRejectedCallbacks = []

const resolve = value => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value

// 成功时执行队列中的方法
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = reason => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.REJECTED
this.reason = reason

// 失败时执行队列中的方法
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then (onFulfilled, onRejected) {
if (this.status === ENUM_STATUS.FULFILLED) {
onFulfilled(this.value)
}
if (this.status === ENUM_STATUS.REJECTED) {
onRejected(this.reason)
}
if (this.status === ENUM_STATUS.PENDING) {
// 便于扩展 采用AOP模式
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason)
})
}
}
}

以上代码加入了异步处理的逻辑,同时实现了 then 方法可以进行多次调用,但是目光敏锐的同学会问:既然可以多次调用,那可以进行链式调用吗?确实在 Promise 中,then 的链式调用是 Promise 的核心之一,正是因为支持链式调用从而解决了无限回调的问题,更多细节如下:

  1. 可以在 then 方法中的成功回调和失败回调抛出异常,走到下一个 then 方法的失败回调中。
  2. 如果返回的是 Promise, 则会把当前 Promise 的状态作为结果向下传递。
  3. 错误处理遵循就近原则,向下找到即执行。

对于 then 方法链式调用的思考

比如jQuery链式调用是通过return this来实现的,但是对于 Promise 来说这样是行不通的。Promise 是通过返回一个新的 Promise 来实现的。原因是什么呢?我们知道 Promise 有一个重要的特点是:如果成功则不能失败,如果失败则不能成功。打个比方:现有一段有 5 个 then 方法调用的代码片段,如果 Promise 使用同一个实例(return this),第一个 then 抛出异常,则会直接进入失败的方法中(catch),其余的 then 均没有被执行。但是如果 then 返回的是一个新的 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
42
43
44
45
46
47
48
49
class Promise {
constructor (executor) {
this.status = ENUM_STATUS.PENDING
this.value = undefined
this.reason = undefined

this.onResolvedCallbacks = []
this.onRejectedCallbacks = []

const resolve = value => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = reason => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then (onFulfilled, onRejected) {
let promise2 = new Promise((resolve, reject) => {
if (this.status === ENUM_STATUS.FULFILLED) {
let x = onFulfilled(this.value)
}
if (this.status === ENUM_STATUS.REJECTED) {
let x = onRejected(this.reason)
}
if (this.status === ENUM_STATUS.PENDING) {
this.onResolvedCallbacks.push(() => {
let x = onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
let x = onRejected(this.reason)
})
}
})
return promise2
}
}

2.2.7 then must return a promise. promise2 = promise1.then(onFulfilled, onRejected);
2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [Resolve].
2.2.7.2 If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

如上代码修改,根据Promises/A+规范所述(2.2.7),定义了then方法返回的promise2和成功或失败的返回值x, 这个x需要通过resolve或者reject进行处理,所以要知道x是成功还是失败,需要一个方法进行判断,我们新增了resolvePromise()方法。

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
then (onFulfilled, onRejected) { 
let promise2 = new Promise((resolve, reject) => {
if (this.status === ENUM_STATUS.FULFILLED) {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
}
if (this.status === ENUM_STATUS.REJECTED) {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
}
if (this.status === ENUM_STATUS.PENDING) {
this.onResolvedCallbacks.push(() => {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
})
this.onRejectedCallbacks.push(() => {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
})
}
})
return promise2
}

const resolvePromise = (x, promise2, resolve, reject) => {}

如下,则要实现resolvePromise方法,来解析x和promise2的关系,但是这样写还会有一个问题,仔细看会发现我们调用resolvePromise方法时的参数传入了promise2,但是我们对该方法的调用是写在promise2实例化方法体里的,这样会导致执行时报错,提示我们不能在promise2初始化完成前使用promise2。文档里也有讲:

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick.

我们可以使用宏任务setTimeout去进行包装,使onFulfilled和onRejecte都可以异步执行。既然用宏任务包装了一层,错误处理肯定也会丢失,所以这里我们在用setTimeOut的同时,也要使用try catch进行包装。

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
42
43
44
45
46
47
then (onFulfilled, onRejected) {
let promise2 = new Promise((resolve, reject) => {
if (this.status === ENUM_STATUS.FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === ENUM_STATUS.REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === ENUM_STATUS.PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
})
return promise2
}

继续实现resolvePromise方法,这里有一点要注意的是,我们需要兼容其他版本或者其他人写的promise,虽然都是按照Promises/A+规范来实现的,但其中还是有一定的差异。需要注意的另外一点是规范中指出:如果promise和x指向同一个object,则通过reject promise抛出一个类型错误作为失败的原因。

这里将规范文档中的重点筛出来,作为代码实现的注释:

2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
2.3.2 If x is a promise, adopt its state.
2.3.3 Otherwise, if x is an object or function,
2.3.3.1 Let then be x.then.
2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
2.3.3.3 If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where:
2.3.3.3.1 If/when resolvePromise is called with a value y, run [Resolve].
2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
2.3.3.3.3 If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
2.3.3.3.4 If calling then throws an exception e,
2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, ignore it.
2.3.3.3.4.2 Otherwise, reject promise with e as the reason.
2.3.3.4 If then is not a function, fulfill promise with x.
2.3.4 If x is not an object or function, fulfill promise with x.

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
42
43
44
45
46
47
const resolvePromise = (x, promise2, resolve, reject) => {
if (x === promise2) {
// 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
// 我买几个橘子去。你就在此地,不要走动。
reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// 兼容其他人的Promise,则Promise可能是对象也可能是函数。
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// 2.3.3.3.3 此处解析的x如果是Promise,则可能是其他类库的Promise, 即存在一种不确定性: 此Promise既调用了成功也调用了失败。所以需要定义变量called来规避此种情况。
let called
try {
// 2.3.3.1 Let then be x.then.
// 取 then 方法
let then = x.then

// then的类型如果是function, 则then是Promise
if (typeof then === 'function') {
// 2.3.3.3 If then is a function, call it with x as this.
// 取then方法的目的是在此处进行复用,防止多次调用get方法(x.then)而导致报错。
// 2.3.3.3.1 --- 2.3.3.3.3 (成功 => y 失败 => r)
then.call(x, y => {
if (called) return
called = true
// y也可能是Promise,则需递归解析y的值直到结果为普通值。
resolvePromise(y, promise2, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} else {
// 2.3.3.4 If then is not a function, fulfill promise with x.
// 非Promise的普通对象
resolve(x)
}
} catch (e) {
// 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
if (called) return
called = true
reject(e)
}
} else {
// 2.3.4 If x is not an object or function, fulfill promise with x.
// 非Promise的普通值
resolve(x)
}
}

至此,我们完整实现了resolvePromise方法。

回头看文档

2.2.1 Both onFulfilled and onRejected are optional arguments:
2.2.1.1 If onFulfilled is not a function, it must be ignored.
2.2.1.2 If onRejected is not a function, it must be ignored.

可以了解到then方法的两个参数onFulfilled和onRejected均为可选参数,我们实现时则默认写成了必传的方式,所以此处需要修改,则涉及到一个“穿透”的概念。可以用如下例子进行理解:

1
2
3
4
5
new Promise((resolve, reject) => {
resolve('foo')
}).then().then().then(data => {
console.log(data)
})

如果用我们目前实现的Promise,很明显data的值无法正常打印,所以我们需要进行改写,通过中间的两个then()进行值的穿透,传递到最后一个then中:

1
2
3
4
5
new Promise((resolve, reject) => {
resolve('foo')
}).then(data => data).then(data => data).then(data => {
console.log(data)
})

那么,思路来了:

1
2
3
4
5
6
7
8
9
then (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }

// 此处省略 Promise2的定义
// ...

return promise2
}

这样,我们完整地实现了Promise。接下来需要检测我们实现的Promise是否“扛揍”,Promises/A+文档最后贴上了推荐测试的包:Compliance Tests

测试要求提供一个测试入口,按要求添加(测试完毕后会对此方法进行解读)

1
2
3
4
5
6
7
8
Promise.defer = Promise.deferred = () => {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}

全局安装promises-aplus-tests

1
npm install -g promises-aplus-tests

安全完毕后我们进行测试:

1
promises-aplus-tests promise.js

最后提示:872 passing (16s), 代表测试全部通过。部分截图如下:
promise-test-result.png

接下来,我们解释一下这个延迟对象deferred: Promise的实现,本身是为了解决嵌套问题。比如我们开始封装一个异步方法foo,如果我们在foo方法体中直接new Promise,这样直接开始了一层嵌套,很不友好。为了简化此种写法,让我们封装的方法可以直接promise化调用foo.then(),所以我们可以使用Promise.defer这个静态方法(通过类本身来调用的方法),此方法会返回一个dfd。即在foo方法体开始定义let dfd = Promise.defer(), 方法体结尾return dfd.promise,在foo方法体中的异步方法处理成功失败均可挂载到dfd上:dfd.resolve(data);dfd.reject(error)。如此我们减少了不必要的嵌套,让代码更加优雅。

我们需要对Promise的静态方法catch进行实现,很容易想到:catch相当于一个没有成功的then方法,如下:

1
2
3
catch (errorCallback) {
return this.then(null, errorCallback)
}

另外,还有两个静态方法Promise.resolve()和Promse.reject(), resolve方法即产生一个成功的Promise, reject方法即产生一个失败的Promise:

1
2
3
4
5
6
7
8
9
10
11
static resolve (value) {
return new Promise((resolve, reject) => {
resolve(value)
})
}

static reject (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}

此处如果经过测试会发现一个小问题,这个静态方法resolve的参数value如果是一个Promise(调用了new Promise),在其中的异步方法中又调用了resolve方法,我们就无法拿到正确的结果(不会等待异步方法执行完毕,直接返回Promise且会处在pending状态) 。所以我们就需要对这个value进行递归解析(ES6内部自身已实现了该方法):

1
2
3
4
5
6
7
8
9
10
11
12
// 重写constructor中的resolve方法
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}

if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}

综上,我们实现了Promise.resolve的等待功能。

至此,我们不仅完美实现了符合Promises/A+规范的Promise类库、通过了官方的测试。 还为了对标ES6 Promise, 让Promise更完整,实现了静态方法resolve,reject和catch等方法。

后续计划:

  1. 对Promise.all,Promise.race,Promise.finally进行实现
  2. 对Promise.allSettled(ES2020)进行实现

最后,贴上完整的代码:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
const ENUM_STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}

const resolvePromise = (x, promise2, resolve, reject) => {
if (x === promise2) {
// 我买几个橘子去。你就在此地,不要走动。
reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let called
try {
let then = x.then
if (typeof then === 'function') {
then.call(x, y => {
if (called) return
called = true
resolvePromise(y, promise2, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}

class Promise {
constructor (executor) {
this.status = ENUM_STATUS.PENDING

this.value = undefined
this.reason = undefined

this.onResolvedCallbacks = []
this.onRejectedCallbacks = []

const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}

if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.FULFILLED
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = reason => {
if (this.status === ENUM_STATUS.PENDING) {
this.status = ENUM_STATUS.REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}

try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}

then (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }

let promise2 = new Promise((resolve, reject) => {
if (this.status === ENUM_STATUS.FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === ENUM_STATUS.REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === ENUM_STATUS.PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(x, promise2, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
})

return promise2
}

catch (errorCallback) {
return this.then(null, errorCallback)
}

static resolve (value) {
return new Promise((resolve, reject) => {
resolve(value)
})
}

static reject (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
}

Promise.defer = Promise.deferred = () => {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}

module.exports = Promise

JS的内存管理机制(垃圾回收机制)

发表于 2018-09-30 | 分类于 JavaScript

像C语言这样的高级语言一般都有底层的内存管理接口,比如 malloc()和free()。另一方面,JavaScript创建变量(对象,字符串等)时分配内存,并且在不再使用它们时“自动”释放。 后一个过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者感觉他们可以不关心内存管理。 这是错误的。

内存生命周期

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放\归还
    所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。

JS的内存分配

  1. 值的初始化
    为了不让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var n = 123 // 给数值变量分配内存
    var s = "azerty" // 给字符串分配内存

    var o = {
    a: 1,
    b: null
    } // 给对象及其包含的值分配内存

    // 给数组及其包含的值分配内存(就像对象一样)
    var a = [1, null, "abra"]

    function f(a) {
    return a + 2
    } // 给函数(可调用的对象)分配内存

    // 函数表达式也能分配一个对象
    someElement.addEventListener('click', function() {
    someElement.style.backgroundColor = 'blue'
    }, false)
  2. 通过函数调用分配内存
    有些函数调用结果是分配对象内存:

    1
    2
    var d = new Date() // 分配一个 Date 对象
    var e = document.createElement('div') // 分配一个 DOM 元素

有些方法分配新变量或者新对象:

1
2
3
4
5
6
7
8
9
10
var s = "azerty"
var s2 = s.substr(0, 3) // s2 是一个新的字符串
// 因为字符串是不变量,
// JavaScript 可能决定不分配内存,
// 只是存储了 [0-3] 的范围。

var a = ["ouais ouais", "nan nan"]
var a2 = ["generation", "nan nan"]
var a3 = a.concat(a2)
// 新数组有四个元素,是 a 连接 a2 的结果

使用值

使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。

当内存不再需要使用时释放

大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。

垃圾回收

如上文所述自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。

引用

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

引用计数垃圾收集

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var o = { 
a: {
b:2
}
}
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集

var o2 = o // o2变量是第二个对“这个对象”的引用
o = 1 // 现在,“这个对象”的原始引用o被o2替换了
var oa = o2.a // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo" // 最初的对象现在已经是零引用了
// 他可以被垃圾回收了
// 然而它的属性a的对象还在被oa引用,所以还不能回收
oa = null // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
限制:循环引用

该算法有个限制:无法处理循环引用。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

1
2
3
4
5
6
7
8
9
10
function f() {
var o = {}
var o2 = {}
o.a = o2 // o 引用 o2
o2.a = o // o2 引用 o

return "azerty"
}

f()

例子:
IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:

1
2
3
4
5
6
var div
window.onload = function() {
div = document.getElementById("myDivElement")
div.circularReference = div
div.lotsOfData = new Array(10000).join("*")
}

在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的 DOM 元素,即使其从DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),而这个数据占用的内存将永远不会被释放。

标记-清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

循环引用不再是问题了

在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。

限制: 那些无法从根对象查询到的对象都将被清除

尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。

浏览器的缓存机制总结

发表于 2018-04-16 | 分类于 JavaScript

浏览器缓存类型

有两种,强缓存和协商缓存

  1. 强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码,并且size显示from disk cache或from memory cache;

  2. 协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;

  3. 两者的共同点是:都是从客户端缓存中读取资源;区别是:强缓存不会发请求,协商缓存会发请求。

缓存有关的 header

强缓存
  1. Expires:response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存。

  2. Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。

Expires和Cache-Control:max-age=*** 的作用是差不多的,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。

协商缓存

ETag和If-None-Match:这两个要一起说。Etag是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器接受到If-None-Match的值后,会拿来跟该资源文件的Etag值做比较,如果相同,则表示资源文件没有发生改变,命中协商缓存。

Last-Modified和If-Modified-Since:这两个也要一起说。Last-Modified是该资源文件最后一次更改时间,服务器会在response header里返回,同时浏览器会将这个值保存起来,在下一次发送请求时,放到request header里的If-Modified-Since里,服务器在接收到后也会做比对,如果相同则命中协商缓存。

ETag和Last-Modified的作用和用法也是差不多,说一说他们的区别。

  1. 首先在精确度上,Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

  2. 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。

  3. 第三在优先级上,服务器校验优先考虑Etag。

浏览器缓存过程

  1. 浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;
  2. 下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求;
  3. 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;
  4. 如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200。

用户行为对浏览器缓存的控制

  1. 地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;
  2. F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断;
  3. ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。

关于jQuery和Vue两者技术架构的比较分析报告

发表于 2018-01-19 | 分类于 JavaScript

关于jQuery和Vue两者技术架构的比较分析报告

jQuery

jQuery已经过时了。略做点补充:Zepto也是过时货了。还有Underscore/Lodash等,也是过时了。但是过时不代表你就一定不可以再用,或者要从现有项目中清除抛弃掉。项目维护和管理本身是另一回事情,并不是完全由技术因素决定的。看一下前两点,1. 新的DOM标准(借鉴jQuery)加入了许多新的方法,覆盖了绝大部分use cases;2. 目前主流浏览器的兼容性已经大幅提高,且因为都是Evergreen browsers了,所以以后也不太会出现严重的兼容性问题了;此外新标准比以往要更详尽清晰,出现不一致和bug的机率也小了;实际上这前两点也不是一蹴而就的,而是一直在改进。比如原生querySelector API普及之后,才出现了Zepto。只不过这两年发展加速,以至于Zepto还没取代jQuery,就要一起过时了。(贺师俊hax)

jQuery的核心价值

  1. 发扬光大了$和CSS选择器的天才idea(尽管都不是发明者)
  2. 处理浏览器的兼容性问题和各种bug
  3. 链式调用为核心的DSL(此为jQuery独创)
  4. 基于jQuery的生态(大量插件,各种工具如IDE也对其有良好支持)

jQuery的劣势

  1. 完整的jQuery体积太大。对于一些比较小的项目确实可以做到快速开发,但是现在的jQuery太臃肿了,有很多用不到的功能。所以现在有了很多精简jQuery的项目。另外就是全DOM操作,钩子往往会依赖标签,如果依赖jQuery来搭建页面的话(比如后台输出json,然后jQuery loop一个列表出来),维护上会有困难。如果一改页面结构,很多依赖标签的选择器,一改起来js那块就得跟着大改。还有就是jQuery的代码改起来不容易,如果真有项目特殊需求,要改一下jQuery代码来用就显得很麻烦。
  2. 不能向后兼容。每一个新版本不能兼容早期的版本。举例来说,有些新版本不再支持某些selector,新版jQuery却没有保留对它们的支持,而只是简单的将其移除。这可能会影响到开发者已经编写好的代码或插件。
  3. 对数据的处理有很多不便利的地方,容易导致高耦合度。

jQuery的适用场景

在过去的前端开发中,jQuery几乎会出现在任何大大小小的项目中,不论是类MS,还是电商,还是各类门户网站,都少不了jQuery的身影,可以说在之前的前端开发中,jQuery更是一种“标准”。

Vue

讲Vue之前就讲MVVM吧(Vue的架构模式就是MVVM)

引言

2008年,V8 引擎随 Chrome 浏览器横空出世,JavaScript 这门通用的 Web 脚本语言的执行效率得到质的提升。 V8 引擎的出现,注定是 JavaScript 发展史上一个光辉的里程碑。它的出现,让当时研究高性能服务器开发、长时间一筹莫展的 Ryan Dahl 有了新的、合适的选择,不久,在2009年的柏林的 JSConf 大会上,基于 JavaScript 的服务端项目 Node.js 正式对外发布。Node.js 的发布,不仅为开发者带来了一个高性能的服务器,还很大程度上推动了前端的工程化,带来了前端的大繁荣。与此同时,因为 JavaScript 执行效率的巨大提升,越来越多的业务逻辑开始在浏览器端实现,前端逻辑越来越重,前端架构随之提上日程。于是,我们谈论的主角,MVVM 模式,走进了 Web 前端的架构设计中。

概念

MVVM 模式,顾名思义即 Model-View-ViewModel 模式。它萌芽于2005年微软推出的基于 Windows 的用户界面框架 WPF ,前端最早的 MVVM 框架 knockout在2010年发布。当前最流行了MVVM 框架 Vue 的2.0版本在2016年5月发布。

一句话总结 Web 前端 MVVM:操作数据,就是操作视图,就是操作 DOM(所以无须操作 DOM )。

无须操作 DOM !借助 MVVM 框架,开发者只需完成包含 声明绑定 的视图模板,编写 ViewModel 中业务数据变更逻辑,View 层则完全实现了自动化。这将极大的降低前端应用的操作复杂度、极大提升应用的开发效率。MVVM 最标志性的特性就是 数据绑定 ,MVVM 的核心理念就是通过 声明式的数据绑定 来实现 View 层和其他层的分离。完全解耦 View 层这种理念,也使得 Web 前端的单元测试用例编写变得更容易。

MVVM,说到底还是一种分层架构。它的分层如下:

  • Model: 域模型,用于持久化
  • View: 作为视图模板存在
  • ViewModel: 作为视图的模型,为视图服务

Model 层

Model 层,对应数据层的域模型,它主要做域模型的同步。通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步。在层间关系里,它主要用于抽象出 ViewModel 中视图的 Model。

View 层

View 层,作为视图模板存在,在 MVVM 里,整个 View 是一个动态模板。除了定义结构、布局外,它展示的是 ViewModel 层的数据和状态。View 层不负责处理状态,View 层做的是 数据绑定的声明、 指令的声明、 事件绑定的声明。

ViewModel 层

ViewModel 层把 View 需要的层数据暴露,并对 View 层的 数据绑定声明、 指令声明、 事件绑定声明 负责,也就是处理 View 层的具体业务逻辑。ViewModel 底层会做好绑定属性的监听。当 ViewModel 中数据变化,View 层会得到更新;而当 View 中声明了数据的双向绑定(通常是表单元素),框架也会监听 View 层(表单)值的变化。一旦值变化,View 层绑定的 ViewModel 中的数据也会得到自动更新。

前端 MVVM 图示

mvvm

如图所示,在前端 MVVM 框架中,往往没有清晰、独立的 Model 层。在实际业务开发中,我们通常按 Web Component 规范来组件化的开发应用,Model 层的域模型往往分散在在一个或几个 Component 的 ViewModel 层,而 ViewModel 层也会引入一些 View 层相关的中间状态,目的就是为了更好的为 View 层服务。

开发者在 View 层的视图模板中声明 数据绑定、 事件绑定 后,在 ViewModel 中进行业务逻辑的 数据 处理。事件触发后,ViewModel 中 数据 变更, View 层自动更新。因为 MVVM 框架的引入,开发者只需关注业务逻辑、完成数据抽象、聚焦数据,MVVM 的视图引擎会帮你搞定 View。因为数据驱动,一切变得更加简单。

MVVM框架的工作(优势)

不可置否,MVVM 框架极大的提升了应用的开发效率。It’s amazing!But,MVVM 框架到底做了什么?

  • 视图引擎

视图引擎:我是视图引擎,我为 View 层作为视图模板提供强力支持,开发者,你们不需要操作 DOM ,丢给我来做!

  • 数据存取器

数据存取器:我是数据存取器,我可以通过 Object.defineProperty() API 轻松定义,或通过自行封装存取函数的方式曲线完成。我的内部往往封装了 发布/订阅模式,以此来完成对数据的监听、数据变更时通知更新。我是 数据绑定 实现的基础。

  • 组件机制

组件机制:我是组件机制。有追求的开发者往往希望按照面向未来的组件标准 - Web Components 的方式开发,我是为了满足你的追求而生。MVVM 框架提供组件的定义、继承、生命周期、组件间通信机制,为开发者面向未来开发点亮明灯。

  • more…

劣势

MVVM架构型模式的兴起,实现了前后端真正的职责分离,在提高开发效率的同时,也存在一些不足之处。

  1. 比如说SEO:网站的前后分离架构越来越得到开发者们的喜爱与认可, 后端只提供数据接口、业务逻辑与持久化服务,而视图、控制与渲染则交给前端。 因此,越来越多的网站从后端渲染变成了前端渲染,而由此带来的直接问题就是各大搜索引擎爬虫对于前端渲染的页面( 动态内容 )还无法比较完善的爬取,这就导致了网站的内容无法被搜索引擎收录,直接影响网站流量与曝光度。但是,在如今的Vue中可以使用服务端渲染或者预渲染(SSR or Prerendering)来解决seo的问题,更有SSR开源框架Nuxt的支持。所以,MVVM是应时代产物,在逐步变得更加完善。
  2. 在兼容性方面,Vue由于数据操作API方法的选择的原因,所以并没有做到兼容IE8及其以下版本。

适用场景

可以说前后端分离随着趋势已经形成一种标准,MVVM设计模式的开发框架(Vue)适用任何场景的开发(低版本IE除外)。

总结

jQuery是直接来操作DOM的,凭借简化后的API直接和DOM对话(优异的兼容性);
Vue是直接来操作数据的,拿数据说话。

JavaScript中null和undefined的区别

发表于 2017-11-22 | 分类于 JavaScript

很多人都喜欢问这个问题,然而问这个问题的人本身也不太懂,所以今天拿出来剖析一下这个事情。

一、等价性

在JavaScript中,将一个变量赋值为null或undefined,几乎没区别。

1
2
var foo = null;
var bar = undefined;

null和undefined在if语句中,都会被自动转为false,相等运算符结果是两者相等。

1
2
3
4
5
6
7
if (!null) 
console.log('null is false'); // null is false

if (!undefined)
console.log('undefined is false'); // undefined is false

null == undefined; // true

二、历史原因

1995年JavaScript诞生时,最初像Java一样,只设置了null作为表示”无”的值。
根据C语言的传统,null被设计成可以自动转为0。

1
2
3
4
5
Number(null)
// 0

5 + null
// 5

但是,JavaScript的设计者Brendan Eich,觉得这样做还不够,有两个原因。
首先,null像在Java里一样,被当成一个对象。但是,JavaScript的数据类型分成原始类型(primitive)和合成类型(complex)两大类,Brendan Eich觉得表示”无”的值最好不是对象。
其次,JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。Brendan Eich觉得,如果null自动转为0,很不容易发现错误。
因此,Brendan Eich又设计了一个undefined。

三、设计之初

JavaScript的最初版本是这样区分的:null是一个表示”无”的对象,转为数值时为0;undefined是一个表示”无”的原始值,转为数值时为NaN。

1
2
3
4
5
Number(undefined)
// NaN

5 + undefined
// NaN

四、用法

上面这样的区分,在实践中很快就被证明不可行。目前,null和undefined基本是同义的,只有一些细微的差别。

null

null表示”没有对象”,即该处不应该有值。典型用法是:

(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。

1
2
Object.getPrototypeOf(Object.prototype)
// null

undefined

undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:

(1)变量被声明了,但没有赋值时,就等于undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。

1
2
3
4
5
6
7
8
9
10
11
var i;
i // undefined

function f(x){console.log(x)}
f() // undefined

var o = new Object();
o.p // undefined

var x = f();
x // undefined

Over.

如何在同一客户端部署多个git帐号(多账户下设置ssh keys)?

发表于 2017-11-10 | 分类于 git

有些事情总是无法避免的,要是你没有遇到过这个问题,你可能不知道我在说什么。就比如说我自己的一些开源项目放在github上,然而公司的项目是部署在gitlab上。我们使用git进行版本管理即与远程服务器做到同步,但是问题来了:默认情况下每个账户生成的秘钥位置和名称都是相同的,这样后生成的秘钥就会覆盖前面的秘钥导致其失效。解决的办法就是在生成后一个秘钥的时候对其重新命名,以避免冲突,同时将不同的秘钥配置到相对应的Host上面,这样在访问不同的远程仓库时调用不同的key,冲突也就解决了。

Mac环境为例

生成gitlab密钥

1
ssh-keygen -t rsa -C "注册gitlab的邮箱" -b 4096

使用默认名称,然后不设置密码,回车直接跳过。
查看 .ssh 目录,可以看到生成的公钥和私钥,名称分别为 id_rsa.pub 和 id_rsa

使用命令 cat ~/.ssh/id_rsa.pub 可以查看公钥内容,这个就是gitlab下ssh keys下需要添加的内容。

生成github密钥

1
ssh-keygen -t rsa -C "注册github的邮箱" -b 4096

这次不使用默认名称,要对生成定的秘钥进行重命名,这里重命名为 id_rsa_github , 同样不设置密码。
查看 .ssh 目录,可以看到生成的公钥和私钥,名称分别为 id_rsa_github.pub 和 id_rsa_github

使用命令cat ~/.ssh/id_rsa_github.pub可以查看公钥内容,这个就是github下ssh keys下需要添加的内容。

配置config文件

在 .ssh/ 目录下新建config文件:touch config,通过vim进行如下配置:

1
2
3
4
5
6
7
8
9
# gitlab
Host gitlab
HostName gitlab.foo.com
IdentityFile ~/.ssh/id_rsa

# github
Host github
HostName github.com
IdentityFile ~/.ssh/id_rsa_github

然后:wq (vim命令,保存退出)

Host对应的名称是一个别名,命名可以随意,用来进行远程连接。HostName和IdentityFile是各自主机名称以及对应的秘钥文件。


配置完毕。

还有人在问你caller和callee的区别?

发表于 2017-09-25 | 分类于 JavaScript

很搞笑,首先我不知道面试官还会问这种问题,意义何在?let me give him a fu… lesson?

Function.caller

返回调用指定函数的函数。
如果一个函数f是在全局作用域内被调用的,则f.caller为null,相反,如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数。
该属性的常用形式arguments.callee.caller替代了被废弃的 arguments.caller
什么? 被废弃了?
是的,废弃的 arguments.caller 属性原先用在函数执行的时候调用自身。本属性已被移除且不再有用。
arguments.caller 已经不可使用了,但是你还可以使用 Function.caller,但是该特性也是非标准的,尽量不要在生产环境中使用它!

下例演示了arguments.caller属性的作用

1
2
3
4
5
6
function whoCalled() {
if (arguments.caller == null)
console.log('该函数在全局作用域内被调用.');
else
console.log(arguments.caller + '调用了我!');
}

下例用来得出一个函数是被谁调用的

1
2
3
4
5
6
function myFunc() {
if (myFunc.caller == null) {
return ("该函数在全局作用域内被调用!");
} else
return ("调用我的是函数是" + myFunc.caller);
}

arguments.callee

不用鸡冻,这个东西也好不到哪儿去,
callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。这在函数的名称是未知时很有用,例如在没有名称的函数表达式 (也称为“匿名函数”)内。
但是,看这个警告:

在严格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。
当一个函数必须调用自身的时候, 避免使用 arguments.callee(), 通过要么给函数表达式一个名字,要么使用一个函数声明。

为什么 arguments.callee 从ES5严格模式中删除了?

早期版本的 JavaScript不允许使用命名函数表达式,出于这样的原因, 你不能创建一个递归函数表达式。
例如,下边这个语法就是行的通的:

1
2
3
4
5
function factorial (n) {
return !(n > 1) ? 1 : factorial(n - 1) * n;
}

[1,2,3,4,5].map(factorial);

但是:

1
2
3
[1,2,3,4,5].map(function (n) {
return !(n > 1) ? 1 : /* 这里写什么? */ (n - 1) * n;
});

这个不行。为了解决这个问题, arguments.callee 添加进来了。然后你可以这么做

1
2
3
[1,2,3,4,5].map(function (n) {
return !(n > 1) ? 1 : arguments.callee(n - 1) * n;
});

然而,这实际上是一个非常糟糕的解决方案,因为这 (以及其它的 arguments, callee, 和 caller 问题) 使得在通常的情况(你可以通过调试一些个别例子去实现它,但即使最好的代码是最理想的,你也没必要去检查调试它)不可能实现内联和尾递归。另外一个主要原因是递归调用会获取到一个不同的 this 值,例如:

1
2
3
4
5
6
7
8
9
10
11
12
var global = this;

var sillyFunction = function (recursed) {
if (!recursed) { return arguments.callee(true); }
if (this !== global) {
alert("This is: " + this);
} else {
alert("This is the global");
}
}

sillyFunction();

ECMAScript 3(es3?哈哈) 通过允许命名函数表达式解决这些问题。例如:

1
2
3
[1,2,3,4,5].map(function factorial (n) {
return !(n > 1) ? 1 : factorial(n-1)*n;
});

这有很多好处:

  • 该函数可以像代码内部的任何其他函数一样被调用
  • 它不会在外部作用域中创建一个变量 (除了 IE 8 及以下)
  • 它具有比访问arguments对象更好的性能

另外一个被废弃的特性是 arguments.callee.caller,具体点说则是 Function.caller。为什么? 额,在任何一个时间点,你能在堆栈中找到任何函数的最深层的调用者,也正如我在上面提到的,在调用堆栈有一个单一重大影响:不可能做大量的优化,或者有更多更多的困难。比如,如果你不能保证一个函数 f 不会调用一个未知函数,它就绝不可能是内联函数 f。基本上这意味着内联代码中积累了大量防卫代码:

1
function f (a, b, c, d, e) { return a ? b * c : d * e; }

如果 JavaScript 解释器不能保证所有提供的参数数量在被调用的时候都存在,那么它需要在行内代码插入检查,或者不能内联这个函数。现在在这个特殊例子里一个智能的解释器应该能重排检查而更优,并检查任何将不用到的值。然而在许多的情况里那是不可能的,也因此它不能够内联。

番外:
在匿名递归函数中使用 arguments.callee

递归函数必须能够引用它本身。很典型的,函数通过自己的名字调用自己。然而,匿名函数 (通过 函数表达式 或者 函数构造器 创建) 没有名称。因此如果没有可访问的变量指向该函数,唯一能引用它的方式就是通过 arguments.callee。

下面的例子定义了一个函数,按流程,定义并返回了一个阶乘函数。该例并不是很实用,并且几乎都能够用命名函数表达式实现同样结果的例子

1
2
3
4
5
6
7
8
9
function create() {
return function(n) {
if (n <= 1)
return 1;
return n * arguments.callee(n - 1);
};
}

var result = create()(5); // 返回120 (5 * 4 * 3 * 2 * 1)

没有替代方案的 arguments.callee

下面的例子是没有可以替代 arguments.callee的方案的,因此弃用它时会产生一个BUG (参看 bug 725398):

1
2
3
4
5
6
7
8
9
function createPerson (sIdentity) {
var oPerson = new Function("alert(arguments.callee.identity);");
oPerson.identity = sIdentity;
return oPerson;
}

var joe = createPerson("Joseph");

joe();

利用命名函数表达式也可以实现上述例子的同样效果

1
2
3
4
5
6
7
8
9
10
function createPerson (identity) {
function Person() {
console.log(Person.identity);
}
Person.identity = identity;
return Person;
}
var joe = createPerson("Joseph");

joe(); //Joseph

Over.

服务端跨域

发表于 2017-08-24 | 分类于 JavaScript

一、反向代理服务器

基础思想很简单,将你的服务器配置成 需要跨域获取的资源的 反向代理服务器。
也就是说,将其他域名的资源映射到你自己的域名之下,这样浏览器就认为他们是同源的。
用大家钟爱的 Apache2 来举个例子:
首先启用两个模块 proxy 和 proxy_http 来开启代理功能:

sudo a2enmod proxysudo a2enmod proxy_http

然后在配置文件里面写入:

ProxyPass “/foo” “http://foo.example.com/bar"ProxyPassReverse “/foo” “http://foo.example.com/bar“

ProxyPass: 远程服务器在本地服务器的映射。(上面的例子将 http://foo.example.com/bar映射为 /foo)

ProxyPassReverse: 配置 Apache2 在 HTTP 跳转时调整 Location, Content-Location 和 URI headers的值,防止反向代理被绕开。

重启 Apache2:

sudo service apache2 restart

大功告成,这样我们请求 /foo就会得到 http://foo.example.com/bar的内容了。
这种方法其实不太常用,机智的读者就会发现,每一个资源都要到自己的服务器配置,每次配置都还要重启。

二、CORS

Cross-Origin Resource Sharing 是 W3C 推出的一种跨站资源获取的机制。

首先我们来看一下浏览器的支持情况:
Chrome Firefox (Gecko) Internet Explorer Opera Safari
4 3.5 8 & 9, 10 12 4

移动端的浏览器对这种方法的支持比较完善。现在我们看到了,如果不需要兼容 IE6、7的话,就可以使用这种方法。
这种跨域方案主要的思想是:服务器 在响应头中设置相应的选项,浏览器如果支持这种方法的话就会将这种跨站资源请求视为合法,进而获取资源。
可以设置的响应头信息:

1
2
Access-Control-Allow-Origin
Access-Control-Allow-Origin: <origin> | *

origin: 被允许跨域访问这个资源的网站,* 代表全部网站。浏览器会检测这个参数,如果符合要求,才会去获取资源。
举个例子,允许 http://qiaohongshen.github.io/foo来跨域访问这个资源:

1
2
3
Access-Control-Allow-Origin: http://jasonkid.github.io/fezone
Access-Control-Allow-Credentials
Access-Control-Allow-Credentials: true | false

表示是否允许浏览器携带 Cookie 来访问这个资源。这个属性要和 XMLHttpRequest 的 withCredentials属性来配合使用。

1
2
3
4
5
6
7
8
var xhr = new XMLHttpRequest();
var url = 'http://foo.other/resources/credentialed-content/';
if(xhr) {
xhr.open('GET', url, true);
xhr.withCredentials = true; // 设置带有 Cookie 的资源请求
xhr.onreadystatechange = handler;
xhr.send();
}

能够成功使用带有 Cookie 的资源请求需要满足以下几个条件:

  1. XMLHttpRequest
  2. 对象中指定了 withCredentials = true
  3. 服务器响应头中 Access-Control-Allow-Credentials: true
  4. 服务器响应头中 Access-Control-Allow-Origin不能为 *

以下选项主要是安全性配置的问题,主要是服务器的配置问题了,就不展开介绍了:

  • Access-Control-Expose-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers

再谈前端跨域

发表于 2017-08-01 | 分类于 JavaScript

1. JSONP

首先要介绍的跨域方法必然是 JSONP。
现在你想要获取其他网站上的 JavaScript 脚本,你非常高兴的使XMLHttpRequest 对象来获取。但是浏览器一点儿也不配合你,无情的弹出了下面的错误信息:

XMLHttpRequest cannot load http://x.com/main.dat. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://y.com‘ is therefore not allowed access.

你心里肯定会想,我难道要用后台做个爬虫来获取这个数据吗?!为了避免这种事情发生,JSONP 就派上用场了。
<script>标签是不受同源策略的限制的,它可以载入任意地方的 JavaScript 文件,而并不要求同源。所以 JSONP 的理念就是,我和服务端约定好一个函数名,当我请求文件的时候,服务端返回一段 JavaScript。这段 JavaScript 调用了我们约定好的函数,并且将数据当做参数传入。非常巧合的一点(其实并不是),JSON 的数据格式和JavaScript 语言里对象的格式正好相同。所以在我们约定的函数里面可以直接使用这个对象。光说不练假把式,让我们来看一个例子:
你需要获取数据的页面 index.html:

1
2
3
4
5
6
<script>   
function getWeather(data) {
console.log(data);
}
</script>
<script src="http://x.y.com/xx.js">

http://x.y.com/xx.js 文件内容:

1
2
3
4
getWeather({
"城市": "北京",
"天气": "大雾"
});

我们可以看到,在我们定义了 getWeather(data)这个函数后,直接载入了 xx.js。在这个脚本中,执行了 getWeather函数,并传入了一个对象。然后我们在这个函数中将这个对象输出到 console 中。

这就是整个 JSONP 的流程。

2. document.domain

使用条件:

  1. 有其他页面 window对象的引用
  2. 二级域名相同
  3. 协议相同
  4. 端口相同

document.domain默认的值是整个域名,所以即使两个域名的二级域名一样,那么他们的 document.domain也不一样。使用方法就是将符合上述条件页面的 document.domain设置为同样的二级域名。这样我们就可以使用其他页面的 window对象引用做我们想做的任何事情了。
补充知识:
x.one.example.com 和 y.one.example.com 可以将 document.domain设置为 one.example.com,也可以设置为example.com。document.domain只能设置为当前域名的一个后缀,并且包括二级域名或以上(.edu.cn这种整个算顶级域名)。我们直接操刀演示,用两个网站 http://wenku.baidu.com/ 和http://zhidao.baidu.com/。这两个网站都是 http 协议,端口都是 80, 且二级域名都是 baidu.com。打开http://wenku.baidu.com/,在 console 中输入代码:

1
2
document.domain = 'baidu.com';
var otherWindow = window.open('http://zhidao.baidu.com/');

我们现在已经发现百度知道的网页已经打开了,在百度知道网页的 console 中输入以下代码:

1
document.domain = 'baidu.com';

现在回到百度文库的网页,我们就可以使用百度知道网页的 window对象来操作百度知道的网页了。例如:

1
var divs = otherWindow.document.getElementsByTagName('div');

上面这个例子的使用方法并不常见,但是非常详细的说明了这种方法的原理。这种方法主要用在控制 <iframe>的情况中。
比如我的页面(http://one.example.com/index.html)中内嵌了一个 <iframe>:

1
<iframe id="iframe" src="http://two.example.com/iframe.html"></iframe>

我们在 iframe.html 中使用 JavaScript 将 document.domain设置好,也就是 example.com。在 index.html 执行以下脚本:

1
2
3
4
var iframe = document.getElementById('iframe');
document.domain = 'example.com';
iframe.contentDocument; // iframe的 document 对象
iframe.contentWindow; // iframe的 window 对象

这样,我们就可以获得对iframe的完全控制权了。

补充知识:
当两个页面不做任何处理,但是使用了iframe或者 window.open() 得到了某个页面的 window 对象的引用,我们可以直接访问的属性有哪些?

方法
window.blur
window.close
window.focus
window.postMessage
window.location.replace
属性 权限
window.closed 只读
window.frames 只读
window.length 只读
window.location.href 只写
window.opener 只读
window.parent 只读
window.self 只读
window.top 只读
window.window 只读

3. window.name

我们来看以下一个场景:
随意打开一个页面,输入以下代码:

1
2
window.name = "My window's name";
location.href = "http://www.qq.com/";

再检测 window.name :

1
window.name; // My window's name

可以看到,如果在一个标签里面跳转网页的话,我们的 window.name是不会改变的。基于这个思想,我们可以在某个页面设置好 window.name的值,然后跳转到另外一个页面。在这个页面中就可以获取到我们刚刚设置的 window.name了。由于安全原因,浏览器始终会保持 window.name 是 string类型。这个方法也可以应用到与 <iframe> 的交互上来。我的页面(http://one.example.com/index.html)中内嵌了一个 <iframe>:

1
<iframe id="iframe" src="http://omg.com/iframe.html"></iframe>

在 iframe.html 中设置好了 window.name为我们要传递的字符串。我们在 index.html 中写了下面的代码:

1
2
3
4
5
var iframe = document.getElementById('iframe');
var data = '';
iframe.onload = function() {
data = iframe.contentWindow.name;
};

定睛一看,为毛线报错?细心的读者们肯定已经发现了,两个页面完全不同源啊!由于 window.name 不随着 URL 的跳转而改变,所以我们使用一个暗黑技术来解决这个问题:

1
2
3
4
5
6
7
8
var iframe = document.getElementById('iframe');
var data = '';
iframe.onload = function() {
iframe.onload = function() {
data = iframe.contentWindow.name;
}
iframe.src = 'about:blank';
};

或者将里面的 about:blank 替换成某个同源页面(最好是空页面,减少加载时间)。
补充知识:
about:blank,javascript: 和 data:中的内容,继承了载入他们的页面的源。
这种方法与 document.domain方法相比,放宽了域名后缀要相同的限制,可以从任意页面获取 string类型的数据。

4. [HTML5] postMessage

在 HTML5 中, window 对象增加了一个非常有用的方法:

1
windowObj.postMessage(message, targetOrigin);

windowObj: 接受消息的 Window 对象。
message: 在最新的浏览器中可以是对象。
targetOrigin: 目标的源,* 表示任意。

这个方法非常强大,无视协议,端口,域名的不同。
下面是烤熟的栗子:

1
2
3
4
5
6
7
8
var windowObj = window; // 可以是其他的 Window 对象的引用
var data = null;
addEventListener('message', function(e) {
if(e.origin == 'http://qiaohongshen.github.io/foo') {
data = e.data;
e.source.postMessage('Got it!', '*');
}
});

message事件就是用来接收 postMessage发送过来的请求的。函数参数的属性有以下几个:
origin: 发送消息的 window的源。
data: 数据。
source: 发送消息的 Window对象。

《Vue随笔》 (二)组件之间的数据传递

发表于 2017-07-29 | 分类于 Vue

Vue 的组件作用域都是孤立的,不允许在子组件的模板内直接引用父组件的数据。必须使用特定的方法才能实现组件之间的数据传递。

首先用 vue-cli 创建一个项目,其中 App.vue 是父组件,components 文件夹下都是子组件。
project tree

一、父组件向子组件传递数据

在 Vue 中,可以使用props向子组件传递数据。

子组件部分:

child component

这是 header.vue 的 HTML 部分,logo 是在 data 中定义的变量。

如果需要从父组件获取 logo 的值,就需要使用props: [‘logo’]
c com

在 props 中添加了元素之后,就不需要在 data 中再添加变量了

父组件部分:

f coms

在调用组件的时候,使用 v-bind 将 logo 的值绑定为 App.vue 中定义的变量 logoMsg
app com

然后就能将App.vue中 logoMsg 的值传给 header.vue 了。

二、子组件向父组件传递数据

子组件主要通过事件传递数据给父组件

子组件部分:

c com

这是 login.vue 的 HTML 部分,当<input>的值发生变化的时候,将 username 传递给 App.vue

首先声明一个了方法setUser,用 keyup 事件来调用 setUser
c com

在 setUser 中,使用了$emit来遍历transferUser事件,并返回 this.username

其中transferUser是一个自定义的事件,功能类似于一个中转,this.username将通过这个事件传递给父组件

父组件部分:

f com

在父组件 App.vue 中,声明了一个方法 getUser,用 transferUser 事件调用 getUser 方法,获取到从子组件传递过来的参数 username
app com

getUser 方法中的参数 msg 就是从子组件传递过来的参数 username

三、子组件向子组件传递数据

Vue 没有直接子对子传参的方法,建议将需要传递数据的子组件,都合并为一个组件。如果一定需要子对子传参,可以先从传到父组件,再传到子组件。

为了便于开发,Vue 推出了一个状态管理工具 Vuex ,可以很方便实现组件之间的参数传递。

12
Joseph

Joseph

“为往圣继绝学”

17 日志
3 分类
5 标签
GitHub 简书
© 2015 — 2021 Joseph