-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsearch.xml
More file actions
404 lines (404 loc) · 477 KB
/
search.xml
File metadata and controls
404 lines (404 loc) · 477 KB
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[Vue生命周期详解]]></title>
<url>%2F2018%2F06%2F13%2FVue%2FVue%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E8%AF%A6%E8%A7%A3%2F</url>
<content type="text"><![CDATA[理解 Vue 的生命周期是尤为重要的,不仅是在于对 Vue 的理解,或者是实际项目中,对 Vue 的使用,都离不开 Vue 的生命周期。 Vue 生命周期首先,先来看看Vue官网这张图( Vue 2.x 生命周期) 一切都是从创建 Vue 的实例开始的,实例创建完成后,进行数据响应、模板编译、指令绑定、数据渲染等操作。 主要有这些钩子函数:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestory、destoryed 来看下简单的 demo,看下 Vue 生命周期的这些钩子函数到底做了哪些事 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Vue 生命周期</title></head><body> <div id="app"> {{message}} </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> <script> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' }, beforeCreate () { console.log('实例创建之前') console.log(this.$el) console.log(this.$data) }, created () { console.log('实例创建之后') console.log(this.$el) console.log(this.$data) }, beforeMount () { console.log('挂载到实例之前') console.log(this.$el) console.log(this.$data) }, mounted () { console.log('挂载到实例之后') console.log(this.$el) console.log(this.$data) }, beforeUpdate () { console.log('数据更新之前') console.log(this.$el) console.log(this.$data) }, updated () { console.log('数据更新之后') console.log(this.$el) console.log(this.$data) }, beforeDestroy () { console.log('实例销毁之前') console.log(this.$el) console.log(this.$data) }, destroyed () { console.log('实例销毁之后') console.log(this.$el) console.log(this.$data) } }) </script></body></html> 执行上述的 demo,输出如下: 12345678910111213141516171819实例创建之前undefinedundefined实例创建完成之后undefined{__ob__: Observer}挂载到实例之前<div id="app"> {{message}}</div>{__ob__: Observer}挂载到实例之后<div id="app"> Hello Vue!</div>{__ob__: Observer} 在浏览器控制台输入1app.message = 'Hello World!' 1234567891011121314// 输出如下数据更新之前<div id="app"> hello world</div>{__ob__: Observer}数据更新之后<div id="app"> hello world</div>{__ob__: Observer} 在浏览器控制台输入1app.$destroy() 12345678910111213141516app.message = 'Hello World!'// 输出如下实例销毁之前<div id="app"> hello world</div>{__ob__: Observer}实例销毁之后<div id="app"> hello world</div>{__ob__: Observer} 小结通过上述代码,我们可以对 Vue 的生命周期有了大概的了解, beforeCreate 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用,此时 el 和 data 还未被赋值 created 在实例创建完成后被立即调用,会进行数据观测 (data observer),属性和方法的运算,watch/event 事件回调。 beforeMount 对 el 赋值,但数据还未装载 mounted 数据已装载,el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 beforeUpdate 发生在虚拟 DOM 打补丁之前 updated 当这个钩子被调用时,组件 DOM 已经更新,并不是所有子组件都会更新 beforeDestroy 实例被销毁之前调用,此时,实例仍可用 destroyed 实例被销毁之后调用,此时,实例不可用,但生成的 DOM 仍然存在,只是无法更改 DOM 中的数据 进一步深入仅仅了解是不够,接下来我们通过源码来深入了解下 Vue 的生命周期 12345678// vue/src/core/instance/index.js// 当执行 new Vue() 时,会执行以下方法initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue) initMixin先来看看 initMixin(Vue) 方法 12345678910111213141516// vue/src/core/instance/init.jsexport function initMixin (Vue: Class<Component>) {// ...initLifecycle(vm) // 初始化生命周期initEvents(vm) // 初始化事件initRender(vm) // 初始化渲染callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm) // 初始化 Vue 实例中的data、watch、computedinitProvide(vm) // resolve provide after data/propscallHook(vm, 'created')// ...} 在 调用 beforeCreate 之前,Vue 进行了生命周期、事件、渲染 render 的初始化 在 beforeCreate 与 created 调用之间,进行了 injections 、state 、provide 的初始化 injections 和 provide,主要为高阶插件/组件库提供用例,成对出现,用于父级组件向下传递数据。这里不展开详述,重点看 initState() 方法。 1234567891011121314151617// vue/src/core/instance/state.jsexport function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) // 初始化父组件传递的参数 if (opts.methods) initMethods(vm, opts.methods) // 初始化 Vue 实例中的方法 if (opts.data) { initData(vm) // 初始化数据,并对 data 的各个属性进行遍历,然后通过 proxy 对 data 进行代理监听 } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) // 初始化 Vue 实例的计算属性 if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) // 初始化 watch }} initState() 主要是对 data/props/computed/watch 等进行初始化,主要调用了以下方法 initProps(),通过遍历 props 的值,验证传入的 props 类型是否正确,然后通过 defineReactive() (内部使用的是 object.defineProperty()) 将 props 转化为当前实例的响应式属性 initMethods(), 遍历 methods 属性,将 methods 绑定到 Vue 实例 initData(),遍历 data 属性,调用 proxy() (内部使用的是 object.defineProperty())对 data 进行数据劫持,然后调用 observe() 对 data 进行监听 initComputed(),建立一个新的watcher,遍历 computed ,通过 getter 对数据进行监听,如果 computed 中的属性,并不存在于 Vue 实例(data),那么会调用 defineComputed()(内部使用的是 object.defineProperty())将数据转换为响应式属性 initWatch(),遍历调用 createWatcher() 创建 watcher,createWatcher() 内部调用 vm.$watch() 方法进行注册监听事件。 mount接下来是 mount 过程 12345678// vue/src/core/instance/init.jsexport function initMixin (Vue: Class<Component>) {// ... if (vm.$options.el) { vm.$mount(vm.$options.el) }} vm.$mount() 的作用是手动地挂载一个未挂载的实例 $mount 方法主要在两个地方: vue/src/platforms/web/runtime/index.js 1234567Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating)} 作用是通过 el 获取相应的DOM元素,然后调用 lifecycle.js 文件中的 mountComponent 方法 vue/src/platforms/web/entry-runtime-with-compiler.js 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950// 缓存了来自 web-runtime.js 的 $mount 方法const mount = Vue.prototype.$mountVue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean): Component { el = el && query(el) // 获取相应的DOM元素// 不允许将 el 挂载到 body 或 html if (el === document.body || el === document.documentElement) { // ... return this } const options = this.$options if (!options.render) { // 没有 render 选项,则解析 template 或 el 并转换为渲染函数render let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) // ... } } else if (template.nodeType) { template = template.innerHTML } else { // ... return this } } else if (el) { // 没有 template,有 el,则根据 el ,获取 template template = getOuterHTML(el) } if (template) { // ... // 根据 template,转换为 render const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render // 将转换的 render 方法挂载到 vm.$options options.staticRenderFns = staticRenderFns // ... } } // 调用被缓存了来自 web-runtime.js 的 $mount 方法 return mount.call(this, el, hydrating)} 作用主要就是根据 template 或者 el ,生成 render 函数,然后将 render 挂载到 vue 实例中,最后调用 lifecycle.js 文件中的 mountComponent 方法 那么 mountComponent 具体做了什么呢? 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748// vue/src/core/instance/lifecycle.jsexport function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean): Component { vm.$el = el // 在Vue实例对象上添加 $el 属性,指向挂载点元素 if (!vm.$options.render) { // 如果没有 render 选项,则将 createEmptyVNode 赋值给 vm.$options.render vm.$options.render = createEmptyVNode // ... } // 调用 beforeMount 钩子函数 callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { // ... } else { // 更新组件 updateComponent = () => { vm._update(vm._render(), hydrating) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { // 是否已挂载 callHook(vm, 'beforeUpdate') // 调用 beforeUpdate 钩子函数 } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook // 如果是第一次 mount 则触发 mounted 生命周期钩子 if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') // 调用 mounted 钩子函数 } return vm} Vue compile 过程直接进入 Vue complier 的源码 1234567891011121314151617// vue/src/complier/index.jsexport const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions): CompiledResult { const ast = parse(template.trim(), options) // 根据 template 字符串,生成 AST(抽象语法树) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options) // 将 AST 编译成可执行的代码字符串 return { ast, render: code.render, staticRenderFns: code.staticRenderFns }}) 这个函数就是导出一个可以创建 complier 的方法 parse接下来看看 Vue 的 complier 是如何根据 template ,解析出 AST 1const ast = parse(template.trim(), options) 1234567891011121314// vue/src/complier/parser/index.jsexport function parse ( template: string, options: CompilerOptions): ASTElement | void { // ... parseHTML(template, { // ... 其他配置 options }) // ...} 主要就是调用了 parseHTML() 方法对 template 进行解析的,其内部主要是使用了一个 while 循环,对传入的 template 字符串进行遍历,根据 HTML 标签进行拆分,然后创建 AST generate根据生成的 AST,又是如何编译成可执行代码串的呢? 1const code = generate(ast, options) 12345678910111213// vue/src/complier/codegen/index.jsexport function generate ( ast: ASTElement | void, options: CompilerOptions): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns }} AST 经过 generate得到 render 函数,render 的返回值是 VNode ,VNode 是 Vue 的虚拟 DOM 节点 编译完成之后,就进行构成 Virtual DOM,然后进行相关 diff 过程,最后更新实际的 DOM 节点 关于 Virtual DOM 和 Vue Diff 过程,请看另一篇博文 Vue 底层原理 参考 Vue 技术内幕]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>Vue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Node.js-Event-Loop]]></title>
<url>%2F2018%2F06%2F09%2FNode%2FNode-js-Event-Loop%2F</url>
<content type="text"><![CDATA[Node 事件循环(Event-Loop) Node.js 是通过事件驱动来服务 I/O 的,而事件循环是 Node 实现异步非阻塞 I/O 的基础 Node 事件循环是基于 libuv 实现的,而内部实现,其实就是一个 while 语句块Node 程序启动时,则会初始化事件循环 这里需要说明下,Node 事件循环与浏览器环境下 JavaScript 的事件循环是完全不一样的 浏览器环境下 JS 事件循环 Node 事件循环是分阶段的,有 update time、timer、 pending (I/O) callbacks、idle, prepare、poll(轮询)、check、close callbacks 七个阶段 而这七个阶段都是在核心函数 uv_run() 中进行 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970// Node 事件循环核心函数int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) // 检查当前循环是否存活 uv__update_time(loop); // 如果没有则更新时间并结束当前循环,进入下一个循环 // Node 事件循环从一个 while 循环开始 while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); // 更新时间 uv__run_timers(loop); // 执行到期的定时器(setTimeout 和 setInterval)回调(往往是之前n轮循环中设置的定时器) ran_pending = uv__run_pending(loop); // 处理异步 I/O,如网络请求 I/O、文件请求 I/O 等 uv__run_idle(loop); uv__run_prepare(loop); // idle 和 prepare 是 Node 内部的一些操作,跟事件循环没有什么关联 timeout = 0; // 设置 poll 阶段的时间变量 if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); // 计算 timeout 时间,用于后面 poll 阶段的操作 uv__io_poll(loop, timeout); // 执行 poll 阶段的相关回调,此阶段有可能会阻塞 uv__run_check(loop); // 执行 setImmediate 回调 uv__run_closing_handles(loop); // 执行回调函数关闭的方法,如关闭文件描述符 // Node默认的mode是 `UV_RUN_ONCE` if (mode == UV_RUN_ONCE) { uv__update_time(loop); // 更新时间 uv__run_timers(loop); // 执行到期的定时器回调 } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } // while 循环结束 if (loop->stop_flag != 0) loop->stop_flag = 0; return r;}// 计算 poll 阶段的时间参数int uv_backend_timeout(const uv_loop_t* loop) { if (loop->stop_flag != 0) // 事件循环是否结束的标志,如果结束则返回0 return 0; if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) // 是否有异步任务,没有则返回0 return 0; if (!QUEUE_EMPTY(&loop->idle_handles)) return 0; if (!QUEUE_EMPTY(&loop->pending_queue)) return 0; // QUEUE_EMPTY(idle_handles)和QUEUE_EMPTY(pending_queue) // 判断前面阶段注册的异步任务队列是否为空,则不为空直接返回0,执行这些异步任务回调 if (loop->closing_handles) // 循环进入关闭阶段,返回0 return 0; // 如果上述条件都不成立,那么 poll 阶段将等待第一个到期的 timer,如果在极短的时间(32.767毫秒)内没有到期的 timer ,则结束等待,进入 下一个阶段(check阶段) return uv__next_timeout(loop);} 各阶段简述 update time:更新时间 timer:执行到期的定时器回调函数 pending (I/O) callbacks:执行延迟到下一个循环迭代的 I/O 回调 idle, prepare:Node 内部执行的 poll:检索新的 I/O事件;执行I / O相关的回调函数(几乎所有函数的回调函数,除了定时器和setImmediate);此外,适当时,节点将在此处阻塞 check:执行 setImmediate 回调 close callbacks:一些关闭回调,如 socket.on(‘close’, …) poll 阶段(重点理解)poll阶段有两个功能: 当 timers 到达指定的时间后,执行指定的 timer 的回调 处理 poll 队列的事件 1、当进入到 poll 阶段,并且没有 timers 被调用的时候,会发生下面的情况: 如果 poll 队列不为空,Event Loop 将同步的执行 poll queue 里的 callback ,直到 queue 为空或者执行的 callback 到达上限 如果 poll 队列为空,则会发生下面的情况: 如果脚本调用了 setImmediate(),Event Loop 将会结束 poll 阶段并且进入到 check 阶段执行 setImmediate() 的回调 如果脚本没有被 setImmediate() 调用,Event Loop 将会等待(此时会阻塞极短时间32.767毫秒)回调被添加到队列中,然后立即执行它们 2、当进入到 poll 阶段,并且调用了 timers 的话,会发生下面的情况: 一旦 poll queue 是空的话,Event Loop 会检查是否 timers,如果有1个或多个 timers 时间已经到达,Event Loop 将会回到 timer 阶段并执行那些 timers 的 callback (即进入到下一次 tick)。 Pending callbacks(即I/O callbacks)被调用,大多数情况下,所有的 I/O callbacks 都是在 poll for I/O(即 poll 阶段)后理解调用的。然而,有些情况,会在下一次 tick 调用,以前被推迟的 I/O callback 会在下一次 tick 的 I/O 阶段调用。 在 Event Loop 完成一个阶段,然后到另一个阶段之前,Event Loop 将会执行这 Next Tick Queue 以及 MicroTask Queue 里面的回调,直到这两个队列为空。一旦它们空了后,Event Loop 会进入到下一个阶段 Next Tick Queue的优先级高于MicroTask Queue setImmediate() vs setTimeout()setImmediate和setTimeout()是相似的,但取决于它们何时被调用,其行为方式不同。 setImmediate()用于在当前 poll(轮询)阶段完成后执行脚本 setTimeout()设置在一定时间(以毫秒为单位)后运行 定时器执行的顺序取决于它们被调用的上下文如果在 I/O 周期内这两个被调用,则 setImmediate 回调总是先执行12345678910111213const fs = require('fs');fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });});// immediate// timeout 如果它们不在 I/O 周期内被调用,则两者执行顺序是不确定的 process.nextTick() vs setImmediate()process.nextTick() 属于 idle 观察者,setImmediate() 属于 check 观察者,idle 观察者优先级大于 I/O 观察者,I/O 观察者优先级大于 check 观察者,因此 process.nextTick() 总是先执行 参考 The Node.js Event Loop, Timers, and process.nextTick()]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>Node</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript数组方法]]></title>
<url>%2F2018%2F06%2F01%2FJavaScript%2FJavaScript%E6%95%B0%E7%BB%84%E6%96%B9%E6%B3%95%2F</url>
<content type="text"><![CDATA[前言:在 JavaScript 中,数组是很重要的数据类型,在日常工作中也必不可少,但是对于数组的各种方法 API 却了解不多,或者是只知其名,不知其何用。本文主要是归纳总结各种数组方法,包括 ES5 和 ES6 中新增的 数组数组对象是一个有序的数据(数据可以是 原始类型 或 对象类型)集合。相对于变量,数组可用于在一个变量中存储多个变量值。数组中的每一项都有一个数字附于其上,被称为索引,索引从0开始计数。 数组允许最后一个元素仍然带有逗号 1[1, 2, 3,] 创建数组 采用 Array 对象进行创建 123var arr1 = new Array() // 创建空数组var arr2 = new Array(10) // 创建长度为10的数组,初始值都为 undefinedvar arr3 = new Array('a', 'b', 'c') // 创建包含3个字符串的数组 数组字面量 1var arr = [1, 2, 3] 数组修改器方法(变异方法)修改器方法(变异方法),指的是会改变被这些方法调用的原始数组 copyWith()浅复制数组的一部分到同一数组中的另一个位置,并返回它 arr.copyWithin(target[, start[, end]]) 接收三个参数,target 是复制到该位置,会替换原来的元素;start 是开始复制的位置;end 是结束复制的位置 12['a', 'b', 'c', 'd'].copyWith(1, 2, 3) // 从索引2开始复制到索引3,也就是复制元素‘c’,到目标位置——索引1// 输出 ['a', 'c', 'c', 'd'] fill()用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。 arr.fill(value[, start[, end]]) 接收三个参数,value 是用来填充的值;start 是开始填充的索引,默认为0;end 是结束填充的索引,默认是数组长度 12[1, 2, 3, 4].fill(0, 2, 3)// 输出[1, 2, 0, 4] pop()常用的数组方法,从数组中删除最后一个元素,并返回这个元素,改变数组长度 12[1, 2, 3, 4].pop()// 输出4,数组变为 [1, 2, 3] push()常用的数组方法,在数组的末尾增加一个或多个元素,并返回数组的新长度 12[1, 2, 3, 4].push(5, 6)// 输出数组长度6 基于数组的 push 和 pop 方法,可实现栈的入栈和出栈操作 reverse()将数组反转,并返回该数组的引用。 12[1, 2, 3].reverse()// [3, 2, 1] shift()删除数组的第一个元素,并返回这个元素 12[1, 2, 3].shift()// 输出 1,数组变为 [2, 3] 基于数组的 push 和 shift 方法,可实现队列的入队和出队操作 unshift()常用的数组方法,在数组的开头增加一个或多个元素,并返回数组的新长度 12[1, 2, 3, 4].push(5, 6)// 输出数组长度6 sort()对数组元素进行排序,并返回当前数组,默认排序顺序是根据字符串Unicode码顺序;可传入用来指定按某种顺序进行排列的函数。 1234567['c', 'b', 'a'].sort()// 输出 ['a', 'b', 'c'][3, 2, 5, 4, 1].sort((a, b) => { return a - b})// 输出 [1, 2, 3, 4, 5] splice应重点关注的数组方法,可对数组进行删除/增加元素。 返回由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。 arr.splice(start[, deleteCount[, item1[, item2[, …]]]]) start 是开始修改的位置索引,deleteCount 是删除数组元素的个数,后续参数是指新插入的数组元素 12[1, 2, 3, 4, 5].splice(2, 2, 0, 0)// 输出被删除元素组成的数组[3, 4],原数组变为 [1, 2, 0, 0, 5] 数组访问方法(非变异方法)数组访问方法(非变异方法),不会改变调用它们的对象的值,只会返回一个新的数组或者返回一个其它的期望值 concat()将当前数组与多个数组或非数组值连接,返回一个新数组 12[1, 2, 3].concat(4, 5)// 输出新数组 [1, 2, 3, 4, 5],原数组不变 includes()判断当前数组是否包含某指定的值,如果是返回 true,否则返回 false arr.includes(searchElement, fromIndex) 接收两个参数,查找的值,开始查找的索引 12[1, 2, 3].includes(2)// 输出 true join()将数组转换成字符串,可传入参数来分隔数组的每个元素,默认为逗号 12['a', 'b', 'c'].join('+')// 'a+b+c' slice()截取当前数组中的一段元素组合成一个新数组,并返回这个新数组 arr.slice(begin, end) 最多可接收两个参数,开始截取的位置索引和结束截取的位置索引 12['a', 'b', 'c'].slice(1)// 输出 ['b', 'c'] 注意 slice() 和 splice() 的区别,slice 不改变原数组,splice 会改变原数组 toString()返回一个由所有数组元素组合而成的字符串。遮蔽了原型链上的 Object.prototype.toString() 方法。 12['a', 'b', 'c'].toString()// 'a,b,c' toLocaleString()返回一个由所有数组元素组合而成的本地化后的字符串。遮蔽了原型链上的 Object.prototype.toLocaleString() 方法。 indexOf()返回数组中第一个与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1 接收两个参数,查找的值,开始查找的索引 12[1, 2, 3, 2, 5].indexof(2)// 输出索引1 lastIndexOf()返回数组中最后一个(从右边数第一个)与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1 接收两个参数,查找的值,开始查找的索引 12[1, 2, 3, 2, 5]. lastIndexOf(2)// 输出索引3 迭代(遍历)方法forEach()对数组中的每个元素执行一次提供的函数 forEach 中设置的函数,可传入三个值:currentValue(数组当前项), index(数组当前项索引), array(操作的数组) 1234['a', 'b', 'c', 'd'].forEach((item, index, array) => { console.log(`${item}的索引是${index}`) console.log(array[index] === item)}) every()测试数组的所有元素是否都通过了指定函数的测试,返回 true 或 false 1234[1, 2, 3, 4].every((item) => { return item > 2})// 输出 false some()测试数组中的某些元素是否通过由提供的函数实现的测试 1234[1, 2, 3, 4].some((item) => { return item > 2})// 输出 true filter()将所有在过滤函数中返回 true 的数组元素放进一个新数组中并返回 1234[1, 2, 3, 4].filter((item) => { return item > 2})// 输出新数组 [3, 4] map()创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果 1234[1, 2, 3].map(item => { return item * 2})// 返回新数组 [2, 4, 6],原数组不变 其他技巧 1234var str = '12345'Array.prototype.map.call(str, function(x) { return x}).reverse().join('') reduce()从左到右为每个数组元素执行一次回调函数,并把上次回调函数的返回值放在一个暂存器中传给下次回调函数,并返回最后一次回调函数的返回值 reduce 方法可传入两个参数,callback 和 初始值(initialValue)。 callback 函数可传入四个参数:accumulator(上一次调用回调时返回的累积值),currentValue(数组正在处理的值),currentIndex(数组当前处理的元素的索引),array(被处理的数组) 123456[1, 2, 3, 4].reduce((accumulator, currentValue) => { return accumulator + currentValue}, 5)// 5 + 1 + 2 + 3 + 4// 输出 15 其他技巧 reduce 是十分灵活并且十分实用的数组方法,应该完全掌握它! 计算数组中每个元素出现的次数 12345678910['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'].reduce((allNames, name) => { if (name in allNames) { allNames[name]++ } else { allNames[name] = 1 } return allNames}, {})// 输出 { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 } 数组去重 1234567[1,2,1,2,3,5,4,5,3,4,4,4,4].sort().reduce((init, current) => { let length = init.length if (length == 0 || init[length - 1] !== current) { init.push(current) } return init}, []) 数字添加千分位分隔符 1234567let num = 123456789let str = num + '' // 转为字符串str.split('').reverse().reduce((previousValue, currentValue, currentIndex) => { return (currentIndex % 3 ? currentValue : currentValue + ',' ) + previousValue})// 输出 123,456,789 reduceRight()从右到左为每个数组元素执行一次回调函数,并把上次回调函数的返回值放在一个暂存器中传给下次回调函数,并返回最后一次回调函数的返回值 reduceRight 跟 reduce 方法类似,只是遍历顺序是从右到左,而 reduce 是从左到右 123456var a = ['1', '2', '3', '4', '5']var left = a.reduce((prev, cur) => { return prev + cur})var right = a.reduceRight((prev, cur) => { return prev + cur})console.log(left) // "12345"console.log(right) // "54321" find()返回数组中满足提供的测试函数的第一个元素的值,如果都不符合,那么返回 undefined 1234[1, 2, 3, 4].find((item) => { return item > 2})// 输出新数组 3 findIndex()返回数组中满足提供的测试函数的第一个元素的索引,如果都不符合,那么返回 -1 1234[1, 2, 3, 4].findIndex((item) => { return item > 2})// 输出索引 2 keys()返回一个数组迭代器对象,该迭代器会包含所有数组元素的键。 123456let arr = ["a", "b", "c"]let iterator = arr.keys()console.log(iterator.next()) // 输出 Object {value: 0, done: false}console.log(iterator.next()) // 输出 Object {value: 1, done: false}console.log(iterator.next()) // 输出 Object {value: 2, done: false} values()返回一个数组迭代器对象,该迭代器会包含所有数组元素的值。 这是个实验性功能,主流浏览器尚未支持,Chrome 及Firefox可以用”arrSymbol.iterator“方法来代替values()方法 123456let arr = ["a", "b", "c"]let iterator = arr.values()console.log(iterator.next()) // 输出 Object {value: 0, done: false}console.log(iterator.next()) // 输出 Object {value: 1, done: false}console.log(iterator.next()) // 输出 Object {value: 2, done: false} entries()返回一个数组迭代器(Array Iterator)对象,该迭代器会包含所有数组元素的键值对 123456let arr = ["a", "b", "c"]let iterator = arr.entries()console.log(iterator.next().value) // 输出[0, "a"]console.log(iterator.next().value) // 输出[0, "b"]console.log(iterator.next().value) // 输出[0, "c"] 其他方法Array.from()从类数组对象或者可迭代对象中创建一个新的数组实例 1Array.from('abc') // 输出 ['a', 'b', 'c'] 经典案例,数组去重 12let arr = [1, 2, 2, 3, 2, 4]Array.from(new Set([...arr])) Array.isArray()用来判断某个变量是否是一个数组对象 1234Array.isArray([1, 2, 3]); // trueArray.isArray({foo: 123}); // false Array.of()根据一组参数来创建新的数组实例,支持任意的参数数量和类型 12345Array.of(7) // [7] Array.of(1, 2, 3) // [1, 2, 3]Array(7) // 长度为7,元素都为 undefined的数组 [ , , , , , , ]Array(1, 2, 3) // [1, 2, 3] 参考文档:Array In MDN]]></content>
</entry>
<entry>
<title><![CDATA[JavaScript_原型_原型链_继承]]></title>
<url>%2F2018%2F05%2F21%2FJavaScript%2FJavaScript-%E5%8E%9F%E5%9E%8B-%E5%8E%9F%E5%9E%8B%E9%93%BE-%E7%BB%A7%E6%89%BF%2F</url>
<content type="text"><![CDATA[原型每一个对象都有一个显式原型(prototype)和一个隐式原型(proto)。隐式原型引用了创建这个对象的函数的原型 prototype 1fn.__proto__ === Fn.prototype 一个对象实例的 proto 和 创建这个对象实例的函数的 prototype 是一样的 123typeof Object === 'function' // Object 是一个构造函数,用于创建对象typeof Object.prototype === 'object' Object.proto 指向的是 Function.prototype (Object 是一个对象构造函数,是被函数 Function 创建的) Foo.proto 指向的也是 Function.prototype (Foo 是一个普通的构造函数,是被函数 Function 创建的) Function.proto 指向的还是 Function.prototype (因为 Function 是被自身创建的) Function.prototype 的 proto 指向的是 Object.prototype typeof Function.prototype === ‘function’ Object.prototype.proto 指向 null 原型链Instanceof 运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。 Instanceof 的判断队则是:沿着A的proto这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。 访问一个对象的属性时,先在自身基本属性中查找,如果没有,再沿着proto这条链向上找,这就是原型链。 call/apply 等方法是在 Function.prototype 中创建的 原型以及原型链图解 继承利用原型以及原型链等方法,实现子类对于父类的继承 12345678910111213// 定义一个动物类function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ console.log(this.name + '正在睡觉!'); }}Animal.prototype.eat = function (food) { console.log(this.name + '正在吃' + food)} 原型链继承将父类的实例作为子类的原型 1234function Cat(){ }Cat.prototype = new Animal();Cat.prototype.name = 'cat'; 构造继承使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型) 1234function Cat (name) { Animal.call(this) // 调用父类的构造函数 this.name = name} 实例继承为父类实例添加新特性,作为子类实例返回 12345function Cat (name) { let obj = new Animal() // 创建父类的实例对象 obj.name = name return obj} 组合继承通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用 1234567function Cat (name) { Animal.call(this) this.name = name}Cat.prototype = new Animal()Cat.prototype.constructor = Cat 寄生组合继承通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点 123456789101112function Cat (name) { Animal.call(this) this.name = name}function inheritPrototype (subType, superType){ var prototype = Object.create(superType.prototype); //创建对象 prototype.constructor = subType; //增强对象 subType.prototype = prototype; //指定对象}inheritPrototype(Cat, Animal); 详细请看之前一篇博文红宝书系列读书笔记(二) 多重继承多重继承,指的是一个子类对象继承了多个父类对象,主要是利用 call/apply 实现多重继承 123456789101112131415161718192021222324let inheritPrototype = function(superType, subType) { return function () { superType.call(this, arguments[0]) for (let key in subType) { if (subType.hasOwnProperty(key)) { this[key] = subType[key] } } }}function Animal (name) { this.name = name}let Cat = inheritPrototype(Animal, { say: function () { console.log(this.name) }})let cat = new Cat('Hello Kitty')cat.say() // 输出 'Hello Kitty 上述代码中,inheritPrototype() 接收两个参数:父类构造函数和父类对象。通过 call 方法将父类构造函数绑定到子类中,然后再将另一个父类对象自身的属性全都复制给子类。 如果通过多个父类构造函数继承多个父类对象,那么代码如下 1234567891011121314151617181920212223function Base1 () { this.name = 'peter'}function Base2 () { this.sayName = function () { console.log(this.name) }}let inheritPrototype = function() { let args = [].slice.call(arguments), len = args.length return function () { for (let i = 0; i < len; i++) { args[i].call(this) } }}let objCreate = inheritPrototype(Base1, Base2)let obj = new objCreate()obj.sayName() // 输出 'peter' ES5 与 ES6 继承的区别ES5 和 ES6 实现的继承底层还是利用了原型链 ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this))ES6 的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改 this。 ES5 的继承时通过原型或构造函数机制来实现。 ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。 super 关键字指代父类的实例,即父类的 this 对象。]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>JavaScript核心</tag>
</tags>
</entry>
<entry>
<title><![CDATA[React 浅析]]></title>
<url>%2F2018%2F05%2F20%2FReact%2FReact%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%2F</url>
<content type="text"><![CDATA[React 生命周期主要有三个过程:装载(Mount)过程、更新(Update)过程、卸载(Unmount)过程 首次装载组件时,执行 constructor、getDefaultProps、getInitialState、componentWillMount、render、componentDidMount 卸载组件时,执行 componentWillUnmount 重新装载组件时,getInitialState、componentWillMount、render、componentDidMount 更新渲染组件时,Props更新,执行 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate State更新,执行 shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate getDefaultProps 和 getInitialState 只有用 React.createClass 方法创造的组件类才会发生作用,getInitialState 在组件的整个生命周期中只会被调用一次 getDefaultProps 是通过 Constructor 进行管理,因此也是整个生命周期中最先开始执行。getDefaultProps 方法只执行一次,这样所有实例初始化的 props 将会被共享。 render 函数是一个纯函数,并不做实际的渲染动作,只是返回一个 JSX 对象,最终由 React 操作渲染过程 setState 更新机制当调用 setState 时,会对 state 以及 _pendingState 更新队列进行合并操作,但其实真正更新 state 的幕后黑手是replaceState。 replaceState 会先判断当前状态是否为 MOUNTING,如果不是即会调用 ReactUpdates.enqueueUpdate 执行更新。 当状态不为 MOUNTING 或 RECEIVING_PROPS 时,performUpdateIfNecessary 会获取 _pendingElement、_pendingState、_pendingForceUpdate,并调用 updateComponent 进行组件更新。 如果在 shouldComponentUpdate 或 componentWillUpdate 中调用 setState,此时的状态已经从 RECEIVING_PROPS -> NULL,则 performUpdateIfNecessary 就会调用 updateComponent 进行组件更新,但 updateComponent 又会调用 shouldComponentUpdate 和 componentWillUpdate,因此造成循环调用,使得浏览器内存占满后崩溃。 解密setState调用 setState 方法更新 State 时,不会立即生效,会推入 pending 队列,然后判断当前处于的阶段是否在 batch Update,如果是,则将该组件推入 dirtyComponents 中。如果不是,则会遍历所有的 dirtyComponents,调用 updateComponent 更新 pending state or props React DiffReact diff 会计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染。 Diff 策略 DOM 节点跨层级的移动操作极少 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同的树形结构。 对于同一层级的一组子节点,它们可以通过唯一 key 进行区分。 基于以上三个策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化。 Tree DiffReact 对树进行分层比较,两棵树只会对同一层次的节点进行比较。 React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对同一层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。 如果有跨层级的 DOM 节点操作,React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。 Component Diff 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。 Element Diff当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。 新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置。 参考知乎专栏 pure render —— React源码剖析系列]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>React</tag>
</tags>
</entry>
<entry>
<title><![CDATA[《深入浅出Node.js》笔记]]></title>
<url>%2F2018%2F05%2F12%2FNode%2F%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BANodejs%2F</url>
<content type="text"><![CDATA[Node 简介Node 是单线程的,基于事件驱动的,是基于事件循环进行执行的,还具有异步 I/O、跨平台等特点 单线程和多线程单线程串行依次执行,单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁,也没有线程上下文交换所带来的性能上的开销。 多线程并行完成,多线程的好处是可以有多条线程来处理不同的任务,可以充分利用 CPU,而无须像单线程那样需要等待一个任务完成后,才能继续下一个任务。 模块机制Node 采用的是 CommonJS 规范,采用同步 require 为啥是同步 require ? 因为 Node 遵循 CommonJS 规范。当时模块加载的方案只有 CommonJS 和 AMD,而考虑到 Node 的模块是来自本地文件系统,加载更快,所以选择了 CommonJS CommonJS 是同步加载,同步执行 AMD 是异步加载,异步执行(前置依赖) CMD 是异步加载,同步执行(就近依赖) ES6 模块是异步加载,同步执行 CommonJS 对模块的定义分为三部分:模块引用、模块定义、模块标识 模块分为两类: Node 自身提供的核心模块 核心模块在 Node 源码的编译过程中,被编译进了二进制执行文件。不需要完整文件定位和编译执行 用户编写的文件模块(包括 node_modules) 文件模块是在运行时动态加载,需要完整的路径分析、文件定位、编译执行 Node 对引入过的模块会进行缓存,缓存的是编译执行后的对象,模块加载会优先从缓存中加载核心模块的缓存检查先与文件模块的缓存检查 模块加载执行的步骤:路径分析、文件定位、编译执行 路径分析根据 require 的路径,区分核心模块、文件模块、自定义模块 核心模块的寻址 如果是核心模块,则直接返回该模块(如果不是第一次引入该模块,则会从缓存中引入) (路径形式)文件模块的寻址 将 require 中的路径转换为真实路径,并以真实路径为索引,将编译执行后的结果存放到缓存中,以使下次加载时更快 (非路径)自定义模块的寻址 当前目录下的 node_modules 目录父目录下的 node_modules 目录父目录的父目录下的 node_modules 目录逐级向上查找,一直到根目录下的 node_modules 目录 文件定位步骤 文件扩展名分析 Node 会按 .js、.json、.node 的次序补足扩展名,依次尝试。在尝试过程中,调用 fs 模块同步阻塞式地判断文件是否存在。非 js 文件最好带上文件后缀,能够稍微优化文件定位的效率。 目录分析和包 查找当前目录下的 package.json,通过 JSON.parse() 解析出包描述对象,从中取出 main 属性指定的文件名进行定位 模块编译定位到具体的文件后,Node 会新建一个模块对象,然后根据路径载入并编译。 .js 文件,通过 fs 同步读取并编译.node 文件,通过 dlopen() 方法加载最后编译生成的文件.json 文件,通过 fs 同步读取,用 JSON.parse 解析其余文件会被当做 js 文件处理 每个编译成功的模块都会将其文件路径作为索引缓存在 Module._cache 对象上 JavaScript 模块的编译在编译过程中,Node 对获取的 JavaScript 文件内容做了头尾包装。在头部添加了 (function (exports, require, module, filename, dirname) { \n,在尾部添加了 \n})123456(function (exports, require, module, __filename, __dirname) { var math = require('math') exports.area = function (radius) { return Math.PI * radius * radius; }}) 包装后的代码会通过 runInThisContext() 方法执行(类似于eval) exports 和 module.exports exports 是 module.exports 的引用。exports 是通过形参传递的,在函数作用域内修改该值,并不会改变作用域之外的值。 C/C++ 模块的编译Node 调用 process.dlopen() 方法进行加载和执行,不需要编译 JSON 文件的编译Node 调用 fs 同步读取 JSON 文件内容之后,用 JSON.parse 解析得到对象,然后将它赋值给模块对象的 exports 核心模块分为两部分:C/C++编写部分,存放在 Node 项目的 src文件夹;JavaScript 编写部分,存放在 Node 项目,存放在 lib 文件夹 在编译所有 C/C++ 文件之前,编译程序会将所有 JavaScript 模块文件编译为 C/C++ 代码 核心模块的引入过程 require(‘os’) => NativeModule.require(‘os’) => process.binding(‘os’) => get_builtin_moduel(‘node_os’) => NODE_MODULE(node_os, reg_func) JavaScript 核心模块主要职责 作为 C/C++ 内建模块的封装层和桥接层,供文件模块调用 纯粹的功能模块 总结Node require 寻址 分析路径,判断引入的模块时核心模块还是文件模块, 如果是核心模块,则直接返回该模块(如果不是第一次引入该模块,则会从缓存中引入) 如果是文件模块(以’./‘、’/‘、’../‘等引入),则首先会根据该模块的父模块,确定该模块的绝对路径,将该模块当做文件处理,依次查找 X,X.js,X.json、X.node,如果找到,则返回该文件,不再继续 将该模块当做文件夹处理,则依次查找’X/package.json’,’X/index.js’,’X/index.json’,’X/index.node’,只要其中有一个存在,就返回该文件,不再继续执行 如果该模块不带路径,根据该模块所在的父模块,确定该模块可能的安装目录。依次在每个目录中,将该模块当成文件名或目录名加载。 require 其实是调用了 Module._load 方法 1234567891011121314151617181920212223242526272829303132Module._load = function(request, parent, isMain) { // 计算绝对路径 var filename = Module._resolveFilename(request, parent); // 第一步:如果有缓存,取出缓存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; // 第二步:是否为内置模块 if (NativeModule.exists(filename)) { return NativeModule.require(filename); } // 第三步:生成模块实例,存入缓存 var module = new Module(filename, parent); Module._cache[filename] = module; // 第四步:加载模块 try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } // 第五步:输出模块的exports属性 return module.exports;}; 异步 I/O 多线程的代价在于创建线程和执行期线程上下文切换的开销较大。 单线程串行同步执行,会因阻塞 I/O 导致其他资源得不到最优的使用 Node 利用单线程,远离多线程死锁、状态同步等问题;利用异步 I/O ,让单线程远离阻塞,以更好利用 CPU。 Node 的异步 I/O 是 libuv 和线程池(或 IOCP)来实现的 异步I/O过程 由上图可知:当发起一个异步调用时,会封装一个请求对象,JavaScript层传入的参数和当前方法都会被封装在这个请求对象中,回调函数则被设置在这个对象的 oncomplete_sym 属性上,封装完成后,则将该请求对象推入线程池中等待执行。 当线程池中有可用线程时,则会执行请求对象的 I/O 操作,然后将执行完成的结果放在请求对象中,然后通知 IOCP 调用完成,I/O 观察者获取到调用结果,然后事件循环时会从 I/O观察者中取出可用的请求对象,然后从请求对象中取出回调函数和结果,调用执行。 请求对象是异步 I/O 过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及 I/O 操作完毕后的回调处理。 Node 异步 I/O 模型的基本要素是事件循环、观察者、请求对象、I/O 线程池 process.nextTick VS setImmediate两者十分类似,都是将回调函数延迟执行,但两者又有所不同: process.nextTick 优先与 setImmediate。前者属于 idle 观察者,后者属于 check 观察者。在事件循环中,idle 观察者先于 I/O 观察者,I/O 观察者先于 check 观察者 process.nextTick 的回调函数保存在一个数组中,setImmediate 的结果是保存在链表中。 process.nextTick 在每轮循环中会将数组中的回调函数全部执行完,setImmediate 在每轮循环中执行链表中的一个回调函数。 异步编程异步编程的多种方法,请查看另一篇文章 Node 自身提供的 events 模块就是发布/订阅模式的简单实现。Node 默认一个事件不可添加超过10个侦听器。(调用 emitter.setMaxListeners(0)取消这个限制) 内存控制V8 的内存限制V8 默认默认堆大小限制是:64位系统 1.4GB,32位系统 0.7GB,这是因为 V8 的内存管理机制一开始只运用于浏览器的应用场景,同时也是考虑到 V8 的垃圾回收效率 可以在程序启动时,添加参数来改变这个限制 123node --max-old-space-size=1700 test.js// 或者node --max-new-space-size=1024 test.js V8 的垃圾回收机制V8 的垃圾回收策略主要基于分代式垃圾回收机制。 将内存分为新生代和老生代 新生代中的对象为存活时间较短的对象 老生代中的对象为存活时间较长或常驻内存的对象 在新生代中,主要通过 Scavenge 算法进行垃圾回收。 新生代中,将内存分为两个空间,一个 From 空间(处于使用状态),一个 To 空间(处于闲置状态) 当开始进行垃圾回收时,检查 From 空间中的存活对象,存活对象会被复制到 To 空间中,而非存活对象将会被释放。完成复制后,From 空间和 To 空间的角色进行对换。 如果一个对象经过多次复制仍然存活,它将会被认为生命周期较长的对象,会晋升到老生代 对象晋升的两个条件:对象经历过 Scavenge 回收、To 空间的内存占用比(25%)超过限制 在老生代中,主要采用了 Mark-Sweep(标记清除) 和 Mark-Compact (标记整理)相结合的方式进行垃圾回收 在老生代中,遍历所有对象,并标记存活对象。随后清除没有被标记的对象。 进行标记清除后,会产生内存空间不连续的状态,因为采用 Mark-Compact 算法,将存活对象往一端移动,移动完成后,直接清理边界外的内存。 内存泄漏通常,造成内存泄漏的原因有如下几个: 缓存 队列消费不及时 作用域未释放 采用 node-heapdump、node-memwatch 等对内存泄漏进行排查 大内存应用通过 Node 的内置模块 stream 模块处理大文件。 理解 BufferBuffer 是一个典型的 JavaScript 与 C++ 结合的模块,主要用于处理二进制数据。 Buffer 类的实例类似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存。 Buffer 的大小在被创建时确定,且无法调整。 new Buffer() 等方法已被废弃。通过 Buffer.from(), Buffer.alloc(), and Buffer.allocUnsafe() 等方法创建 Buffer 网络编程Node 提供了 net、dgram、http、https 这4个模块,分别用于处理 TCP、UDP、HTTP、HTTPS。 TCP传输控制协议,在 OSI 模型中属于传输层。 OSI 七层模型:应用层、表示层、会话层、传输层、网络层、链路层、物理层 TCP 三次握手(建立连接) 发送端发送 SYN 报文,服务端接收后,向发送端回传 SYN/ACK 报文,发送端最后发送 ACK 报文 TCP 四次挥手(关闭连接) 发送端发送 FIN 报文,接收端接收到后,立即返回 ACK 报文,等待接收端数据全部发送完成后,再向发送端发送 FIN 报文,最后发送端发送 ACK 报文。 为什么客户端最后还要等待2MSL? 保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。 防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中 为什么建立连接是三次握手,关闭连接却是四次挥手呢? 建立连接时,服务器处于 Listen 状态,收到建立连接的请求(SYN报文)后,把 ACK 和 SYN 放在一个报文里发送给客户端。 关闭连接时,接收到客户端 FIN 报文,表示客户端不再发送数据,但仍然可以接收数据,因此,服务端会立即返回 ACK 报文,通知客户己方已接收到关闭请求,但服务端可能还有数据没发送完。最后会再发一个 FIN 报文给客户端。 参考:TCP 三次握手和四次握手详解 TCP 滑动窗口窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段(ACK报文)中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 33, 34},其中 {31} 按序到达,而 {32, 33} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 TCP 流量控制流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。 TCP 拥塞控制如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接受,而拥塞控制是为了降低整个网络的拥塞程度。 TCP 主要通过四种算法来进行拥塞控制:慢启动、拥塞避免、快重传、快恢复。 发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 慢启动与拥塞避免 发送的最初执行慢启动,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 … 注意到慢启动每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢启动门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。 如果出现了超时,则令 ssthresh = cwnd/2,然后重新执行慢启动。 快重传和快恢复 在接收方,要求每次接收到报文段都应该发送对已收到有序报文段的确认(ACK报文) 在发送方,如果收到三个重复确认,那么可以确认下一个报文段丢失。此时执行快重传,立即重传下一个报文段。 在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 TCP 优化TCP 针对网络中的小数据包有一定的优化策略:Nagle 算法。会将小数据包存放到缓存区,缓存区数据达到一定数量或一定时间后,才将将其发出。这也就是 TCP 的粘包现象。 其他优化: 启用 TFO (TCP快速打开) 增大TCP的初始拥塞窗口 慢启动重启 消除不必要的数据传输 实现数据的短距离传输(CDN) 复用 TCP 链接(Nginx 负载均衡,有新连接时,检测是否有空闲连接,有则复用这个连接,没有则新建连接) 详细了解:TCP 优化 UDP用户数据包协议,也属于传输层。UDP 与 TCP 最大的不同是 UDP 不是面向连接的。 TCP VS UDP 协议 连接性 双工性 可靠性 有序性 有界性 拥塞控制 传输速度 量级 头部大小 TCP 面向连接(Connection oriented) 全双工(1:1) 可靠(重传机制) 有序(通过SYN排序) 无, 有粘包情况 有 慢 低 20~60字节 UDP 无连接(Connection less) n:m 不可靠(丢包后数据丢失) 无序 有消息边界, 无粘包 无 快 高 8字节 参考:ElemeFE 的 node-interview HTTP详细请看 HTTP WebSocketNode 与 WebSocket 的完美配合 WebSocket 客户端基于事件的编程模型与 Node 中自定义事件相差无几 WebSocket 实现了客户端与服务端之间的长连接,而 Node 事件驱动的方式十分擅长与大量的客户端保持高并发连接 此外,WebSocket 还具有以下特点 客户端与服务端只建立一个 TCP 连接,可以使用更少的连接 WebSocket 服务器端可以推送数据到客户端,这远比 HTTP 请求响应模式更灵活、更高效 有更轻量级的协议头、减少数据传送量 WebSocket 最早是作为 HTML5 重要特性而出现的。WebSocket 是全新的网络协议,握手部分是由 HTTP 完成的,但并不是基于 HTTP 实现。 WebSocket 协议主要分为两个部分:握手和数据传输。 客户端发起请求,请求头包含 Upgrade 字段和 Connection 字段 Upgrade: websocket Connection: Upgrade 表示请求服务器升级协议为 WebSocket,其中 Sec-WebSocket-Key 用于安全校验 如果成功,则服务端返回 101,表示允许客户端切换协议 WebSocket 传输数据是数据帧协议的,会将数据封装成一帧或多帧数据,然后逐帧发送。 为了安全考虑,客户端需要对发送的数据帧进行掩码处理,服务器一旦收到无掩码帧,连接将关闭。服务端发送到客户端的数据则无须做掩码处理。同理,客户端如果接收到带掩码的数据帧,连接也将关闭。 对 WebSocket 感兴趣的话,还可以看看这篇文章 WebSocket基本原理和心跳机制 网络服务与安全Node 在网络安全上提供了 crypto、tls、https 模块。其中 crypto 主要用于加密解密,tls 和 https 是运用于网络。 TLS/SSLTLS/SSL 是一个公钥/私钥的结构,是一个非对称的结构。Node 底层采用的是 openssl 实现 TLS/SSL 的,为此要生成公钥和私钥可通过 openssl 完成。 TLS/SSL 引入数字证书(CA)来进一步认证。 HTTPSHTTPS实质上是添加了加密(SSL和TLS)和认证(数字证书)机制的HTTP HTTPS采用共享密钥加密和公开密钥加密的混合加密方式 共享密钥加密:又叫对称加密,是指加密、解密都用同一个密钥 公开密钥加密:又叫非对称加密,对外发布公钥,对数据进行加密,然后通过私钥对数据进行解密 HTTPS的加密方式是:对传输的报文数据采用共享密钥进行加密,而共享密钥加密的密钥又被公开密钥进行加密。 这样的好处是,避免了公共密钥加密需要耗费大量的CPU和内存资源,传输较慢的问题,同时又能够确保传输的数据能够安全传输 构建 Web 应用略 玩转进程Node 是运行在单个进程的单个线程上。这带来的好处是:程序状态是单一的,在没有多线程的情况下没有锁、线程同步问题,操作系统在调度时也因为较少上下文的切换,可以很好地提高 CPU 的使用率 基本概念进程和线程都是一个时间段的描述,是CPU工作时间段的描述 进程:CPU 分配资源的最小单位。每个应用至少有一个进程,每个进程都有自己的独立地址空间。进程之间的通信需要以通信的方式(IPC)进行,进程之间相互不影响。 线程:CPU 调度资源的最小单位。每个进程至少有一个线程,线程之间共享同一个进程的数据,一个线程挂掉,意味着整个进程都挂掉了。 死锁、活锁和饥饿 死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成一种相互等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 活锁:执行的任务没有被阻塞,由于某些条件没有满足,导致一直重复尝试。在单一实体中,执行一个任务,任务失败,继续尝试执行这个任务。在协同中,两个或多个进程相互“礼让”,导致都无法使用资源。 饥饿:某个进程一直等待资源的释放,但资源却被后面一个又一个进程占用,导致了该进程一直无法请求到资源,处于饥饿状态。 多进程架构基于事件的 Node 服务模型存在两个问题:CPU 的利用率和进程的健壮性。 为此,Node 提供了 child_process 模块,实现了多进程架构,能够充分利用 CPU。 Node 在多进程架构中,采用主从模式,进程分为主进程和工作进程。 主进程不负责具体业务的处理,而是负责调度或管理工作进程。工作进程则负责具体的业务处理。 进程间通信(IPC)进程间通信的目的是为了让不同的进程能够互相访问资源并进行协调工作。 实现进程间通信的技术有:命名管道、匿名管道、socket、信号量、共享内存、消息队列、Domain Socket 等。 Node 中实现 IPC 通道的是管道(pipe)技术,具体实现由 libuv 提供,在 windows 下由命名管道实现,nix 系统则采用 *Unix Domain Socket 实现。 父进程在实际创建子进程之前,会创建 IPC 通道并监听它,然后才创建出子进程,并通过环境变量( NODE_CHANNEL_FD )告诉子进程这个 IPC 通道的文件描述符。 子进程在启动过程中,根据文件描述符去链接这个已存在的 IPC 通道,从而完成与父进程之间的连接。 句柄传递 如何让服务器都监听到相同的端口呢? 通常的做法是,各个进程监听不同的端口,主进程监听主端口,主进程对外接收所有的网络请求,然后将这些请求代理到不同端口的子进程上。通过代理,可以避免端口不能重复监听的问题,甚至可以在代理进程上做适当的负载均衡。但是由于进程每接收到一个连接,将会用掉一个文件描述符,代理进程的方案会浪费掉一倍数量的文件描述符,而操作系统的文件描述符是有限的,这样会影响系统的扩展能力。 句柄传递的方案因此诞生。句柄是一种可以用来标识资源的引用,它的内部包含了指向对象的文件描述符 比如句柄可以用来标识一个服务端 socket 对象、一个客户端 socket 对象、一个 UDP 套接字、一个管道等。 发送句柄:主进程接收到 socket 请求后,将这个 socket 直接发送给工作进程,而不是重新与工作进程之间建立新的 socket 连接来转发数据。文件描述符浪费的问题可以通过这样的方式解决。 master.js 12345678var child = require('child_process').fork('child.js')var server = require('net').createServer();server.listen(1337, function () { child.send('server', server) // 发送句柄 server server.close()}) child.js 12345678910111213var http = require('http')var server = http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('handle by child, pid is ' + process.pid + '\n')})process.on('message', function (m, tcp) { // 接收句柄 tcp server if (m == 'server') { tcp.on('connection', function (socket) { server.emit('connection', socket) }) }}) 句柄发送与还原示意图 send() 方法将消息发送到 IPC 管道前,将消息组装成两个对象,一个参数是 handle, 另一个是 message 12345message = { cmd: 'NODE_HANDLE', type: 'net.Server', msg: message} 发送到 IPC 管道中的实际上是要发送的句柄文件描述符,文件描述符实际上是一个整数值。这个 message 对象在写入到 IPC 管道时通过 JSON.stringify() 进行序列化。最终发送到 IPC 通道中的信息都是字符串。send() 方法不能发送任意对象。 连接了 IPC 通道的子进程可以读取父进程发来的消息,将字符串通过 JSON.parse() 解析还原为对象后,才触发 message 事件将消息传递给应用层使用。 集群稳定之路 多个工作进程的存活状态管理 工作进程的平滑重启 配置或静态数据的动态重新载入 进程事件 message 事件:接收消息 error 事件:当子进程无法被复制创建、无法被杀死、无法发送消息时触发该事件 exit 事件:子进程退出时触发该事件。子进程正常退出,事件的第一个参数为退出码,否则为 null 。如果进程通过 kill() 方法被杀死的,会得到第二个参数,它表示杀死进程时的信号 close 事件:在子进程的标准输输出流中止时触发该事件,参数与 exit 相同 disconnect 事件:在父进程或子进程中调用 disconnect() 方法时触发该事件,在调用该方法时,将关闭监听 IPC 通道 自动重启 主进程监听子进程的 exit 事件,然后重新启动一个工作进程来继续服务。 子进程退出时,向主进程发送一个自杀信号,主进程接收到信号后,立即创建一个新的工作进程。从 exit 事件的处理函数中转移到 message 事件的处理函数中。 负载均衡 Node 默认提供的机制是采用操作系统的抢占式策略。进程对到来的请求进行争抢,哪个进程抢到哪个进程服务 Node 还有一种新的策略,Round-Robin,又叫轮叫调度。轮叫调度的工作方式是由主进程接受连接,将其依次分发给工作进程。 状态共享 Node 进程中不宜存放过多数据,会加重垃圾回收的负担,影响性能。可采用第三方数据存储。数据发送改变时,可通过定时轮询和主动通知两种方法来更新数据。 Cluster 模块Cluster 模块是 child_process 和 net 模块的组合应用。Cluster 有以下事件: fork: 复制一个工作进程后触发 online:复制好一个工作进程后,工作进程主动发送一条 online 消息给主进程,主进程接收后,触发该事件 listening:工作进程中调用 listen() (共享了服务端 Socket )后,发送一条 listening 消息给主进程,主进程接收后触发该事件 disconnect:主进程与工作进程之间的 IPC 通道断开后触发 exit:有工作进程退出时触发 setup:cluster.setupMaster() 执行后触发该事件]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>node.js</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript 数据结构与算法]]></title>
<url>%2F2018%2F04%2F10%2FJavaScript%2FJavaScript%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[栈先进后出,采用数组实现。利用数组的 push 和 pop 方法实现入栈、出栈的操作12345678910111213class Stack () { constructor () { this.items = [] } push (item) { this.items.push(item) } pop () { return this.items.pop() }} 队列先进先出,采用数组实现,与栈结构很相似,只是添加和删除元素的位置不同。主要是利用数组的 push 和 shift 方法实现入队、出队的操作1234567891011121314151617class Queue () { constructor () { this.items = [] } enqueue (item) { this.items.push(item) } dequeue () { return this.items.shift() } size () { return this.items.length }} 优先队列可以设置两种优先级:添加优先级、删除优先级1234567891011121314151617181920212223242526272829303132333435363738394041class Queue () { constructor () { this.items = [] } enqueue (elem, priority) { var self = this var queueElem = { elem, priority } if (self.isEmpty()) { self.items.push(queueElem) } else { let added = false for (let i = 0, len = self.items.length; i < len; i++) { if (queueElem.priority < self.items[i].priority) { self.items.splice(i, 0, queueElem) added = true break } } if (!added) { self.items.push(queueElem) } } } dequeue () { return this.items.shift() } isEmpty () { return this.items.length === 0 }} 循环队列——击鼓传花1234567891011121314151617181920function hotPotato (nameList, num) { var queue = new Queue() for (let i = 0, len = nameList.length; i < len; i++) { queue.enqueue(nameList[i]) } let eliminated = '' while (queue.size() > 1) { for (let j = 0; j < num; j++) { queue.enqueue(queue.dequeue()) } eliminated = queue.dequeue() console.log(eliminated + '在击鼓传花游戏中被淘汰') } return queue.dequeue()} 链表采用对象来实现链表中的元素12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879class LinkedList () { constructor (elem) { this.node = { elem, // 当前节点 next: null // 下一个节点的指针 } this.head = null this.length = 0 } append (element) { this.node.elem = element let node = this.node if (head === null) { head = node } else { current = head while (current.next) { current = current.next } current.next = node } this.length++ } removeAt (pos) { if (pos > -1 && pos < this.length) { let current = head, previous, index = 0 if (pos === 0) { head = current.next } else { while (index++ < pos) { previous = current current = current.next } previous.next = current.next } this.length-- return current.elem } else { return null } } insert (pos, elem) { if (pos >= 0 && pos <= this.length) { let current = head, previous, index = 0 this.node.elem = elem if (pos === 0) { this.node.next = current head = this.node } else { while (index++ < pos) { previous = current current = current.next } this.node.next = current previous.next = this.node } this.length++ return true } else { return false } }} 双向链表链表的节点中有两个指针,一个指向前一个节点,另一个指向后一个节点12345var Node = function (elem) { this.elem = elem this.prev = null this.next = null} 集合(Set)ES6 已原生支持 Set 类型,表示一组互不相同的元素(不重复的元素)。采用对象进行模拟集合结构 字典(Map)字典也是存储互不相同的元素,但是存储的是键值对。ES6 已原生支持 Map 类型。采用对象进行模拟字典结构 树树是一种分层数据的抽象模型。树包含了一系列存在父子关系的节点。每个节点(除了根节点)都有一个父节点以及零个或多个子节点 二叉树二叉树中的节点最多只有两个子节点:左侧节点和右侧节点1234567891011121314var Node = { left: null, right: null, data: ''}// 遍历function travelTree (node) { if (node) { console.log(node.data) travelTree(node.left) travelTree(node.right) }} 二叉搜索树二叉搜索树只允许左侧节点存储(比父元素)小的值,右侧节点存储(比父元素)大(或者相等)的值123456789101112131415161718192021222324252627class BinarySearchTree() { constructor (key) { this.node = { key, left: null, right: null } this.root = null } insert (node, newNode) { if (node.key < newNode.key) { if (node.left === null) { node.left = newNode } else { this.insert(node.left, newNode) } } else { if (node.right === null) { node.right = newNode } else { this.insert(node.right, newNode) } } }} 二叉搜索树查找最小值,只需要遍历树的左节点就行;同理,查找最大值,只需遍历树的右节点12345678910111213141516171819function minNode (node) { if (node) { while (node && node.left !== null) { node = node.left } return node.key }}function maxNode (node) { if (node) { while (node && node.right !== null) { node = node.right } return node.key }} 图图是网络结构的抽象模型。图是一组由边连接的节点(或顶点)。一个图G = (V, E)由以下元素组成。 V: 一组顶点E: 一组边,连接V中的顶点 基本术语 由一条边连接在一起的顶点称为相邻顶点。一个顶点的度是其相邻顶点的数量路径是顶点v1, v2,…,vk的一个连续序列,其中vi和vi+1是相邻的简单路径要求不包含重复的顶点如果图中不存在环,则称该图是无环的。如果图中每两个顶点间都存在路径,则该图是连通的图可以是无向的(边没有方向)或是有向的(有向图)如果图中每两个顶点间在双向上都存在路径,则该图是强连通的 图的遍历广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深地访问顶点。 采用队列实现,通过将顶点存入队列中,最先入队列的顶点先被探索 深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点 采用栈实现,通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问 排序算法冒泡排序(复杂度 O(n^2))冒泡排序比较任何两个相邻的项,如果第一个比第二个大,则交换它们,元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。1234567891011121314function bubbleSort (array) { var len = array.length for (let i = 0; i < len; i++) { for (let j = 0; j < len -1; j++) { if (array[j] > array[j + 1]) { swap (j, j + 1) } } }}function swap (index1, index2) { return [array[index1], array[index2]] = [array[index2], array[index1]]} 采用双重循环进行数组遍历,外循环控制在数组中进行了多少次的排序 改进后的冒泡排序12345678910function modifiedBubbleSort (array) { var len = array.length for (let i = 0; i < len; i++) { for (let j=0; j < len - 1 - i; j++ ){ if (array[j] > array[j + 1]) { swap (j, j + 1) } } }} 如果从内循环减去外循环中已跑过的轮数,就可以避免内循环中所有不必要的比较 选择排序(复杂度 O(n^2))选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。12345678910111213141516function selectSort (array) { let len = array.length, indexMin for (let i = 0; i < len - 1; i++) { indexMin = i for (let j = i; j < len; j++) { if (array[indexMin] > array[j]) { indexMin = j } } if (i !== indexMin) { swap(i, indexMin) } } } 插入排序(复杂度O(n^2)插入排序每次排一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了,接着,它和第二项进行比较,第二项是应该待在原位还是插到第一项之前呢?这样,头两项就已正确排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢?),以此类推。1234567891011121314function insetSort (array) { let len = array.length, j, temp for (let i = 1; i < len; i++) { j = i temp = array[i] while (j > 0 && array[j - 1] > temp) { array[j] = array[j - 1] j-- } array[j] = temp }} 排序小型数组时,此算法比选择排序和冒泡排序性能要好。 归并排序(复杂度 O(nlogn))归并排序是一种分治算法,其思想是将原始数组切分成较小的数组,直到每个小数组只有一 个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。采用递归实现1234567891011121314151617181920212223242526272829303132333435363738394041424344function mergeSort (array) { array = mergeSortRec(array)}function mergeSortRec (array) { let len = array.length if (len === 1) { return array } let mid = Math.floor(len / 2), left = array.splice(0, mid), right = array.splice(mid, len) return merge(mergeSortRec(left), mergeSortRec(right))}function merge (left, right) { let result = [], il = 0, ir = 0 let leftLen = left.length, rightLen = right.length while (il < leftLen && ir < rightLen) { if (left[il] < right[ir]) { result.push(left[il++]) } else { result.push(left[ir++]) } } while (il < leftLen) { result.push(left[il++]) } while (il < rightLen) { result.push(left[ir++]) } return result} 快速排序(复杂度 O(nlogn))快速排序也是分治的思想,将原始数组分为较小的数组。基本思想是找一个基准值,根据这个基准值将一个数组分成两个子数组,逐渐递归123456789101112131415161718192021function quickSort (array) { let len = array.length, pivotIndex = Math.floor( len / 2) pivot = array.splice(pivotIndex, 1)[0] let left = [], right = [] if (len <= 1) { return } arr.forEach((item) => { if (item < qivot) { left.push(item) } else { right.push(item) } }) return quickSort(left).concat([qivot], quickSort(right))} 以上快排算法主要是学习阮一峰老师早期的一篇博文,最近(2018年五月)这个快排算法在知乎上被吐槽了,连带阮一峰老师也被 diss 了。我想说的是,虽然这个算法的空间复杂度和时间复杂度确实不怎样,阮一峰老师这个快排的思想没错,而且对于新人来讲,这个快排算法通俗易懂,在此感谢阮一峰老师,在业内输出了大量的博文(http://www.ruanyifeng.com/blog/),让我们学到了更多。通过此事,其实我们更应该反思,对于现有的知识,我们不能够直接拿来就学,对于知识自己要多实践、多思考。 空间复杂度和时间复杂度更优的快排算法 基本思想:选取数组中一项作为主元,然后创建左右两个指针,分别从数组头部和尾部开始向中间进行遍历。移动左指针,直到找到一个比主元大的元素,接着移动右指针,找到一个比主元小的元素,然后交换这两个元素。重复这个过程,直至左指针超过了右指针,主元左边的元素都比主元小,右边的元素都比主元大。这一操作称为划分 接着对划分后的数组再递归进行划分,直至数组排好序。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950function quickSort () { return quick(arr, 0, arr.length - 1)}function quick (arr, left, right) { let index if (arr.length > 1) { index = partition(arr, left, right) if (left < index -1) { quick(arr, left, index -1) } if (index < right) { quick(arr, index, right) } }}// 划分数组function partition (arr, left, right) { let pivot = arr[Math.floor((left + right)/2)] let i = left, j = right while (i <= j) { while (arr[i] < pivot) { i++ } while (arr[j] > pivot) { j-- } if (i <= j) { swap(arr, i, j) i++ j-- } } return i}// 交换数组元素function swap (arr, i, j) { let temp = arr[i] arr[i] = arr[j] arr[j] = temp} 搜索算法顺序(线性)搜索将每一个数据结构中的元素和我们要找的元素做比较,这是最低效的一种搜索算法。具体实现略 二分搜索二分搜索,首先要求被搜索的数据结构已排序,然后选取中间值,判断中间值与搜索值的关系123456789101112131415161718192021function binarySearch (item) { quickSort() // 对数组进行排序 let low = 0, high = array.length - 1, mid, elem while (low <= high) { mid = Math.floor((low + high) / 2) if (array[mid] < item) { low = mid + 1 } else if (array[mid] > item) { high = mid - 1 } else { return mid } } return -1} 其余算法递归递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题。通常涉及函数调用自身。每个递归函数都必须要有边界条件,即一个不再递归调用的条件(停止点), 以防止无限递归。 实例:斐波那契数列123456789101112131415161718192021222324252627282930313233343536// 递归实现function fib (num) { if (num == 1 || num == 2) { return 1 } return fibonacci(num - 1) + fibonacci(num - 2)}// 非递归实现function fib (num) { let n1 = 1, n2 = 2, n = 1 for (let i = 3; i <= num; i++) { n = n1 + n2 n1 = n2 n2 = n } return n}// ES6 generator 实现function* fib (num) { let [a, b] = [0, 1] while (true) { [a, b] = [b, a + b] yield a }}for (let n of fib()) { if (n > 1000) break console.log(n)} 动态规划动态规划(Dynamic Programming,DP)是一种将复杂问题分解成更小的子问题来解决的优化技术。与分治思想有所不同,分治思想是将问题分解成相互独立的子问题,然后组合它们的答案;动态规划则是将问题分解成相互依赖的子问题。用动态规划解决问题时,要遵循三个重要步骤: 定义子问题实现要反复执行而解决子问题的部分识别并求解出边界条件 利用动态规划解决的一些问题:背包问题、最长公共子序列、短阵链相乘、硬币找零、图的全源最短路径 最少硬币找零问题1234567891011121314151617181920212223242526272829303132333435class MinCoinChange(){ construtcor (coins) { this.coins = coins this.cache = {} } makeChange (amount) { let me = this if (!amount) { return [] } if (me.cache[amount]) { return me.cache[amount] } let min = [], newMin, newAmount for (let i = 0, len = me.coins.length; i < len; i++) { let coin = me.coins[i] newAmount = amount - coin if (newAmount >= 0){ newMin = me.makeChange(newAmount); } if ( newAmount >= 0 && (newMin.length < min.length - 1 || !min.length) && (newMin.length || !newAmount) ) { min = [coin].concat(newMin) console.log('new Min ' + min + ' for ' + amount) } } return (me.cache[amount] = min) }} 贪心算法贪心算法遵循一种近似解决问题的技术,期盼通过每个阶段的局部最优选择(当前最好的解),从而达到全局的最优(全局最优解) 贪心算法解决最少硬币找零问题1234567891011121314151617181920class MinCoinChange(coins){ construtcor (coins) { this.coins = coins } makeChange (amount) { let change = [], total = 0 for (let i = coins.length; i >= 0; i--) { let coin = coins[i] while (total + coin <= amount) { change.push(coin) total += coin } } return change }} 贪心算法对于最少硬币找零问题的解决,是基于硬币数组里的数值是有序的,从大到小排列。此外,比起动态规划算法而言,贪心算法更简单、更快。然而,如我们所见,它并不总是得到最优答案。但是综合来看,它相对执行时间来说,输出了一个可以接受的解。 大O表示法大O表示法,是描述算法的性能和复杂程度。 推导大O阶,我们可以按照如下的规则来进行推导,得到的结果就是大O表示法: 用常数1来取代运行时间中所有加法常数 修改后的运行次数函数中,只保留最高阶项 如果最高阶项存在且不是1,则去除与这个项相乘的常数 算法时间复杂度计算 找出算法中的基本语句 计算基本语句的执行次数的数量级 用大Ο记号表示算法的时间性能(将基本语句执行次数的数量级放入大Ο记号中)]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>JavaScript核心</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Babel 安装使用及基本原理]]></title>
<url>%2F2018%2F03%2F10%2FJavaScript%2FBabel%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%E5%8F%8A%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86%2F</url>
<content type="text"><![CDATA[Babel 官方定义,Babel 是一个 JavaScript 编译器。用于将现在浏览器未兼容的 JavaScript 语法(如:ES6+、JSX、TypeScript等)编译转换为 ES5 语法,使得浏览器能够正常运行代码 Babel 安装及使用安装使用 Babel CLI 工具进行babel编译进入项目目录,安装 babel-cli 以及想要编译转换的类型代码,babel-preset-env 是可以转换 ES6+ 的代码1npm install --save-dev babel-cli babel-preset-env 接着,创建并配置 .babelrc 文件123{ "presets": ["env"] // 设置转码规则} 根据自己项目需求去设置相应的转码规则,如 ES2015,则 install babel-preset-es2015,并设置 .babelrc 文件中 presets 新增’es2015’ 使用在项目目录下,终端执行以下代码1./node_modules/.bin/babel src -d lib 或者在 package.json 中,新增 npm script12345678910 { "name": "my-project", "version": "1.0.0",+ "scripts": {+ "build": "babel src -d lib"+ }, "devDependencies": { "babel-cli": "^6.0.0" } } 结合 Webpack 安装使用1npm install --save-dev babel-loader babel-core babel-preset-env 接着,配置 webpack.config.js12345module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ]} 然后创建并配置 .babelrc 文件123{ "presets": ["env"] // 设置转码规则} 基本原理Babel 是一款 JavaScript 编译器,基本就是将代码以字符串的形式读取,然后进行转换处理,最后返回新的代码字符串主要有以下三个步骤: 解析,读取代码字符串,然后解析构建抽象语法树 转换,将构建好的抽象语法树根据设置的转码规则进行相应的转换 输出,根据转码规则变换后的抽象语法树再重新生成代码字符串输出 创建抽象语法树对代码字符串进行遍历分词,分割出最小的语法单元,如:关键字、标识符、运算符、括号等,将分割出的结果保存在数组中,然后对这个数组中存放的语法单元进行递归遍历,逐渐生成抽象语法树。 转换代码遍历抽象语法树,然后根据转码规则,判断哪一节点需要转换,则进行转换 重新输出代码字符串递归遍历抽象语法树,生成代码字符串]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>JavaScript核心</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HTTP]]></title>
<url>%2F2018%2F03%2F07%2FHTTP%2FHTTP%2F</url>
<content type="text"><![CDATA[HTTP超文本传输协议,基于TCP/IP协议运作,是无连接、无状态的。 三项 WWW 构建技术:HTTP协议、HTML、URL 与HTTP关系密切的协议 IP: 处于网络层,负责把各种数据包传送给对方。IP 间通信依赖 MAC 地址。(使用ARP协议可以根据IP反查出对应的MAC地址) TCP: 位于传输层,通过三次握手确保数据送达目标处。三次握手:发送端首先发送一个带有 SYN 标志的数据包给对方。接收端收到后,回传一个带有 SYN/ACK 标志的数据包以示传达确认信息。最后,发送端再回传一个带 ACK 标志的数据包,表示握手结束。 DNS: 处于应用层,负责域名解析。通过域名查找 IP 地址,或逆向从 IP 地址反查域名。 HTTP 报文HTTP 报文分为请求报文和响应报文 HTTP 请求报文由请求方法、请求URI、协议版本、可选的请求首部字段和内容实体构成 HTTP 响应报文由协议版本、状态码、用以解释状态的原因短语、可选的响应首部字段以及实体主体构成 使用 Cookie 实现 HTTP 的状态管理设置 keep-alive 实现 HTTP 的持久连接 TCP/IP 分层TCP/IP 协议族按层次分别分为以下四个层次:应用层、传输层、网络层和数据链路层 TCP/IP 通信传输流:发送端:应用层 =》传输层 =》网络层 =》链路层 =》 接收端:应用层《= 传输层《= 网络层《= 链路层《= 发送端,每经过一层,都会给数据添加该层的首部;到达接收端时,数据每经过一层,都会解析对应层的首部信息 HTTP 状态码状态码的职责是当客户端向服务端发送请求时,描述返回的请求结果。 1xx:信息性状态码,接收的请求正在处理2xx:成功状态码,请求正常处理完毕3xx:重定向状态码,需要进行附加操作以完成请求(PS: 304是指缓存,不是重定向)4xx:客户端错误状态码,服务端无法处理请求5xx:服务端错误状态码,服务端处理请求出错 HTTP 首部有四种首部:通用首部、请求首部、响应首部、实体首部通用首部: Cache-Control: 控制缓存行为 Connection: HTTP 连接管理,HTTP1.1之后,默认设置为 keep-alive,表示持久连接 Date: 创建报文的时间 Pragma: 历史遗留字段,用于控制缓存 Upgrade: 是否升级为其他协议 Via: 代理服务器的相关信息 请求首部: Accept/Accept-*: 内容协商相关的首部,设置客户端可接收的数据格式等 If-*: 称为条件请求,服务端接收到附带条件的请求后,只有判断条件为 true 时,才会执行请求 UA: 客户端程序信息 Referer: 请求来源 Host: 请求资源所在的服务器(主机) 响应首部: Accept-Range: 是否接受范围请求 ETag: 实体标识符,标记资源信息,用于判断资源缓存是否过期 Location: 令客户端重定向至指定 URI Vary: 代理服务器缓存的管理信息 Server: HTTP 服务器的安装信息 实体首部: Allow: 资源可支持的 HTTP 方法(GET、POST等)Content-*: 实体主体相关的设置,如实体主体的编码格式,自然语言等 Expires: 实体主体过期的日期时间 Last-Modified: 资源的最后修改时间 为 Cookie 服务的首部: Cookie: 服务端接收到的 Cookie 信息,包含了客户端发送的 sessionId (请求首部) Set-Cookie: 开始状态管理的 Cookie 信息,包含了服务端返回的 sessionId (响应首部) 缓存相关首部浏览器缓存分为强缓存和协商缓存 两者主要区别在于:强缓存不会向服务器发起请求,直接返回状态码:200 from cache;协商缓存会向服务器发起请求验证,如果缓存有效,那么返回状态码 304 强缓存相关字段 Pragma:http 1.0 时代,控制缓存的http首部字段 Expires:http 1.0 时代出现,用于设置缓存的时间 Cache-control:http 1.1 新增的,可用于禁止缓存(no-store)、设置缓存失效时间(max-age)no-store 表示禁用缓存,不将资源存储在浏览器中,no-cache表示不使用浏览器中存储的缓存数据(不缓存过期的资源) Pragma、Expires 和 Cache-Control 同时出现,那么会以 Cache-Control 为准 协商缓存相关 If-Modified-Since/LastModified:请求首部 If-Modified 与响应首部 LastModified,标识资源最后更改的时间,如果两者相同,则返回 304 ,不相同则返回 200,然后客户端发起请求最新资源 If-Unmodified-Since/LastModified:表示从某个时间点算起, 文件没有被修改。如果没有被修改,则开始继续传送文件,服务器返回 200;如果文件被修改,则不传输,服务器返回 412(预处理错误) If-None-Match/Etag:请求首部 If-None-Match 与响应首部 Etag,标识资源文件有没有被实质性更改过。Etag 是服务端根据算法,对资源生成的唯一标识,根据这个唯一标识是否相同,判断资源有没有被修改。 Last-Modified 和 ETag 同时被使用,则两者都必须验证通过,才会返回 304 状态码,否则服务端会认为资源缓存失效,重新发送资源给客户端 如果If-Modified-Since的时间比服务器当前时间(当前的请求时间request_time)还晚,会认为是个非法请求 Last-Modified 的缺点:只能精确到秒的级别,一旦在一秒的时间里出现了多次修改,那么这个字段不会发生改变 Etag的缺点:分布式系统中,每台机器生成的 Etag 会不同,导致缓存失效,所以分布式系统尽量关闭掉Etag Expires的缺点:返回的到期时间是服务器端的时间,如果客户端的时间与服务器的时间相差很大,那么误差就很大 通信数据转发程序:代理、网关、隧道代理代理服务器的基本行为就是接收客户端发送的请求后转发给其他服务器。持有资源实体的服务器被称为源服务器。在HTTP通信过程中,从源服务器返回的响应经过多个串联似的的代理服务器,再传给客户端。代理按两种基准分类:一种是是否使用缓存,另一种是是否会修改报文 缓存代理:会预先将资源的副本保存在代理服务器上,当收到客户端对相同资源的再次请求时,会直接从代理服务器中返回透明代理:不对报文做任何加工的代理类型被称为透明代理,反之,称为非透明代理 代理还分为正向代理和反向代理 正向代理:代理客户端向源服务器发起请求的,并将源服务器的响应传回给客户端的代理,称为正向代理,代理的对象是客户端反向代理:对于客户端而言它就像是源服务器,客户端发起请求,反向代理会将请求自动转发给其他服务器。反向代理还可以为后端服务器提供负载均衡,或为后端响应较慢的服务器提供缓冲服务。代理的对象是服务端 网关网关可以将 HTTP 协议转换成其他协议。利用网关能够提高通信的安全性,因为可以在客户端与网关之间的通信线路上加密以确保连接的安全。 隧道隧道的目的是确保客户端与服务端进行安全的通信 HTTPSHTTPS实质上是添加了加密(SSL和TLS)和认证(数字证书)机制的HTTP HTTPS采用共享密钥加密和公开密钥加密的混合加密方式 共享密钥加密:又叫对称加密,是指加密、解密都用同一个密钥 公开密钥加密:又叫非对称加密,对外发布公钥,对数据进行加密,然后通过私钥对数据进行解密 HTTPS的加密方式是:对传输的报文数据采用共享密钥进行加密,而共享密钥加密的密钥又被公开密钥进行加密。这样的好处是,避免了公共密钥加密需要耗费大量的CPU和内存资源,传输较慢的问题,同时又能够确保传输的数据能够安全传输。 数字签名 数字签名技术就是对“非对称密钥加解密”和“数字摘要“两项技术的应用,它将摘要信息用发送者的私钥加密,与原文一起传送给接收者。 接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。 如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。 数字证书颁发过程 用户首先产生自己的密钥对,并将公共密钥及部分个人身份信息传送给认证中心。 认证中心在核实身份后,将执行一些必要的步骤,以确信请求确实由用户发送而来,然后,认证中心将发给用户一个数字证书,该证书内包含用户的个人信息和他的公钥信息,同时还附有认证中心的签名信息(根证书私钥签名)。用户就可以使用自己的数字证书进行相关的各种活动。 CA验证 证书颁发的机构(CA)是伪造的,那么浏览器会认为是危险证书 证书颁发的机构是确实存在的,于是根据CA名,找到对应内置的CA根证书、CA的公钥。用CA的公钥,对伪造的证书的摘要进行解密,发现解不了,认为是危险证书 对于篡改的证书,使用CA的公钥对数字签名进行解密得到摘要A,然后再根据签名的Hash算法计算出证书的摘要B,对比A与B,若相等则正常,若不相等则是被篡改过的。]]></content>
<categories>
<category>HTTP</category>
</categories>
<tags>
<tag>HTTP</tag>
</tags>
</entry>
<entry>
<title><![CDATA[web 安全]]></title>
<url>%2F2018%2F03%2F04%2Fweb%E5%AE%89%E5%85%A8%2Fweb%E5%AE%89%E5%85%A8%2F</url>
<content type="text"><![CDATA[在web中,主要存在数据与指令,当正常数据中掺杂了指令时,则会产生攻击,web安全攻击主要有以下三种:XSS(跨站脚本)攻击、CSRF(跨站请求伪造)攻击、界面操作劫持攻击 web 安全的角色 W3C 浏览器厂商 Web 厂商 攻击者(黑客) 被攻击者(用户) 本地存储方式 Cookie UserData localStorage local DataBase Flash Cookie Web 同源协议同协议、同域名、同端口 XSS 攻击简要概括为:想尽一切办法将脚本内容在目标网站中目标用户的浏览器上解析 XSS 攻击分为:反射型 XSS、存储型 XSS、DOM XSS 反射型 XSS发出请求时,XSS 代码出现在URL上 存储型 XSS提交的 XSS 代码会存储在服务端,下次请求不需要再提交 XSS 代码 DOM XSS不需要服务端解析响应,利用的是浏览器端的DOM解析 XSS 危害 挂马 盗取用户Cookie DoS(拒绝服务)客户端浏览器 钓鱼攻击 XSS 病毒,删除目标文章、恶意篡改数据、嫁祸、“借刀杀人” 劫持 Web 用户行为 XSS 防御 输入校验 输出编码 CSRFCSRF,跨站请求伪造,发起的请求是跨站的,并且是伪造的 按请求类型分为 GET 型和 POST 型 按攻击方式分为:HTML CSRF 攻击、JSON HiJacking 和 Flash CSRF CSRF 的漏洞挖掘1、目标表单是否有有效的 token 随机串。2、目标表单是否有验证码。3、目标是否判断了 Referer 来源。4、网站根目录下 crossdomain.xml 的“allow-access-from domain”是否是通配符。5、目标 JSON 数据似乎可以自定义 callback 函数等。 jsonp 存在 JSON HiJacking 攻击(对返回的 json 数据进行劫持) CSRF 危害 篡改目标网站上的用户数据 盗取用户隐私数据 作为其他攻击向量的辅助攻击手法 传播CSRF蠕虫 CSRF 防御 检查 HTTP Referer 字段是否同域 限制 Session、Cookie 的生命周期 使用验证码 使用一次性 token 当表单提交时,用 JavaScript 在本域添加一个临时的 Cookie 字段,并将过期时间设为1秒之后提交,服务端校验有这个字段即放行,没有则认为是 CSRF 攻击 界面操作劫持这是一种基于视觉欺骗的 web 会话劫持攻击,它通过在网页的可见输入控件上覆盖一个不可见的框(iframe),使用户误以为在操作可见控件,而实际上用户的操作被其不可见的框劫持,执行不可见框中的恶意代码 主要分为:点击劫持、拖放劫持、触屏劫持 界面操作劫持的漏洞挖掘1、目标的 HTTP 响应头是否设置好了 X-Frame-Options 字段2、目标是否有 JavaScript 的 Frame Busting 机制。3、更简单的就是用 iframe 嵌入目标网站试试,若成功,则说明漏洞存在。 界面操作劫持的防御防御这种攻击的思路是:使有重要会话的交互页面不允许被嵌入 iframe,或者只能被同域 iframe 嵌入 X-Frame-Options:判断页面是否可以被嵌入 iframe Frame Busting 脚本防御:使用 js 对页面进行控制,达到页面无法被 iframe 嵌入的目的。通过判断主体窗口地址与当前窗口地址是否一致 使用 token 防御 DoS & DDoS 攻击DoS 指的是拒绝服务攻击 一台或多台计算机对受攻击服务器的某一个端口发送大量无关的数据,导致整个通道内的正常服务无法进行 DDoS 指的是分布式拒绝服务攻击 大量的肉鸡(僵尸计算机)对服务器的不同端口发送巨型流量的数据,无法通过关闭端口的方式来进行隔离,破坏力极强,严重会造成服务器宕机 常见的DoS 攻击类型SYN Flood攻击 利用 TCP 协议的三次握手进行攻击,向服务器发送了SYN报文后,立刻离线,那么服务器在发出SYN + ACK 应答报文后是无法收到客户端的ACK报文的(第三次握手无法完成),这种情况下服务器端会进行重试,然后等待一段时间(SYN Timeout)后才丢弃这个连接。当有大量这样的请求时,服务器会忙着处理这些恶意请求,导致正常请求无法被响应 防范 缩短SYN Timeout时间 设置SYN Cookie,就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,以后从这个IP地址来的包会被一概丢弃 所有的主机平台都有抵御DoS的设置,总结一下,基本的有几种: 关闭不必要的服务 限制同时打开的SYN半连接数目 缩短SYN半连接的time out 时间 及时更新系统补丁 DNS 劫持通过劫持域名解析服务器,或伪造域名解析服务器的方法,把目标网站域名解析到错误的地址从而实现用户无法访问目标网站的目的 DNS(域名解析)的原理 访客向本地 ISP(网络运营商)的 DNS 服务器发起一个域名解析请求 ISP 的 DNS 服务器向授权的DNS服务器发起域名解析请求 授权的 DNS 服务器将结果返回给 ISP 的 DNS 服务器 ISP 的 DNS 将结果返回给用户 用户根据返回结果中的 IP 地址向网站的服务器发起请求 常见攻击方式 域名劫持域名欺骗(缓存投毒),其攻击现象就是利用控制DNS缓存服务器,把原本准备访问某网站的用户在不知不觉中带到黑客指向的其他网站上,其实现方式可以通过利用网民ISP端的DNS缓存服务器的漏洞进行攻击或控制,从而改变该 ISP 内的用户访问域名的响应结果DDoS攻击 防御手段 HTTPDNS,是使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,具有防劫持、精准调度等特性。 避免LocalDNS劫持:由于HttpDNS是通过ip直接请求http获取服务器A记录地址,不存在向本地运营商询问domain解析过程,所以从根本避免了劫持问题 平均访问延迟下降:由于是ip直接访问省掉了一次domain解析过程,通过智能算法排序后找到最快节点进行访问 用户连接失败率下降:通过算法降低以往失败率过高的服务器排序,通过时间近期访问过的数据提高服务器排序,通过历史访问成功记录提高服务器排序。如果ip(a)访问错误,在下一次返回ip(b)或者ip(c) 排序后的记录 HTTP 劫持DNS 解析的域名的IP地址不变,在和网站交互过程中的劫持了你的请求,在网站发给你信息前就给你返回了请求。(各种运营商为了营利而操作的劫持,主要方式为 js 劫持、 iframe 、HTML) 解决 HTTP 劫持的方法 全站替换成 HTTPS 协议 向 ISP 投诉,如果 ISP 不受理,则向工信部举报 DNS 劫持:将你访问的网址解析成另一个网址,如访问 http://www.google.com ,显示是百度的页面HTTP 劫持:正常访问你输入网址的网站,但是这个网站页面中有很多小广告 XSS Proxy 技术XSS Proxy 技术用到了基于浏览器的远程控制上,要实现远程控制,要具备以下两个条件: 远控指令要在目标浏览器上“实时”执行 执行结果要能够让控制端看到 XSS Proxy 有以下四种跨域思路 浏览器 <script> 请求 <script> 标签可跨域,有种基于此标签的跨域数据通信方法,叫 JSONP。但在这过程中,易出现 JSON HiJacking 攻击 跨域 AJAX 请求 服务端 WebSocket 推送 postMessage 方式推送指令 防御手段HTTP 响应的 X-头部与前端安全相关的头部字段有:X-Frame-Options、X-XSS-Protection、X-Content-Security-Policy X-Content-Security-Policy(CSP策略):通过在http的响应头中设定csp的规则,规定当前网页可以加载的资源的白名单,从而减少网页受到XSS攻击的风险 域分离安全传输(HTTPS)安全的Cookie(HttpOnly)验证码]]></content>
<categories>
<category>web 安全</category>
</categories>
<tags>
<tag>web安全</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Node-Interview-QA]]></title>
<url>%2F2018%2F01%2F26%2FNode%2FNode-Interview-QA%2F</url>
<content type="text"><![CDATA[前言:简要翻译 RisingStack 上关于 Node.js 的面试题,包括 2015 年和 2017年 2015 年 RisingStack Node.js 面试题 2017 年 RisingStack Node.js 面试题 1. What is an error-first callback? 什么是错误优先回调?在异步编程的回调函数中,第一个参数一般是传递 error 对象,用于检查程序是否运行正常。正常时,error 对象为 null 。例如 fs.readFile1234567fs.readFile('./test.js', function (err, data) { if (err) { // something went wrong } else { // handle the data }}) 为什么在回调函数中,第一个参数是 error 对象呢?这是一种约定,是 node 设计的原则,当执行回调时,检测程序是否报错,如果存在,则优先处理错误信息,反之继续执行。 2. How can you avoid callback hells? 怎样避免回调地狱? 利用 Promise,ES6 已原生支持 Promise 对象 ES6 的 Generator ES7 提出的 async/await (未标准化) 使用 koa (底层使用的还是 async/await )123456789101112new Promise((resolve, reject) => { setTimeout(() => { resolve('result') }, 100)}) .then(console.log) .catch(console.error)async function handleData () { let data = await getData() // 异步获取数据的方法} 3. How can you listen on port 80 with Node? 如何让 Node 监听 80 端口? Node 程序其实不应该监听 80 端口,需要超级管理员权限,同时这也不是一个好的实践。如果实在是要让程序监听 80 端口,应该使用 Ngnix 一类的反向代理服务器去代理 80 端口HTTP 默认端口是 80,HTTPS默认端口是 443 4. What’s the event loop? 什么是事件循环? Node 支持多线程(通过 libuv ),但是只能运行在单线程环境。每一次 I/O 都会有一个回调函数。当进程启动时,Node 会创建类似 while(true) 循环,每一次循环称为 Tick 。每个 Tick 的过程就是查看任务队列中有没有事件,有则取出事件及相关回调,并执行回调。接着进入下一个 Tick。 5. What tools can be used to assure consistent style? 哪些工具可以让项目代码风格保持一致?ESLint 6. What’s the difference between operational and programmer errors? 运行错误与编码错误有什么区别? 运行错误并不算是 bug,可能是由于系统的一些故障导致的;而编码错误是真正的 bug。 7. Why npm shrinkwrap is useful? 为啥 NPM shrinkwrap很有用? 当开发 Node 应用时,使用 shrinkwrap 可以锁定依赖包的版本。目前推荐使用 yarn ,默认提供了 lock 功能,只要不执行 yarn upgrade,删除后再重新安装模块的版本不会发生变化。 8. What’s a stub? Name a use case. 什么是 stub? 举个栗子 Stub 是用于模拟组件/模块的行为的函数或程序。Stub 在测试用例期间进行模拟的函数调用,并返回虚构的结果。12345678var fs = require('fs')var readFileStub = sinon.stub(fs, 'readFile', function (path, cb) { return cb(null, 'filecontent')})expect(readFileStub).to.be.calledreadFileStub.restore() 9. What’s a test pyramid? How can you implement it when talking about HTTP APIs? 什么是测试金字塔?当涉及到 HTTP APIs 时,要如何实现它? 测试金字塔,指的是编写测试用例时,底层的单元测试要比上层端到端的单元测试多得多。当涉及到 HTTP APIs 时,总结如下 很多针对模型的底层单元测试 当测试模型如何交互时,应减少集成测试 当测试实际的 HTTP 端时,减少验收测试 10. What’s your favourite HTTP framework and why? 无标准答案,自由发挥。目前推荐讲述下 koa 的优缺点,以及与 Express 的对比 11. When are background/worker processes useful? How can you handle worker tasks? 后台进程什么时候有用?你是如何处理后台任务? 当需要在后台进行数据处理时,例如发送电子邮件或处理图像,后台进程很有用。处理耗时的后台任务时,可以通过创建子进程,或者使用一些消息队列,如 RabbitMQ 12. How can you secure your HTTP cookies against XSS attacks? 如何防止 HTTP 的cookies 被 XSS攻击? 设置 HTTP 头部 set-cookie 的属性 HttpOnly 或者 secure1Set-Cookie: name=value; secure; HttpOnly 13. How can you make sure your dependencies are safe? 如何确保引用的依赖包是安全的? 自动执行依赖关系的更新或者对引入的依赖进行安全审查 Trace by RisingStack NSP GreenKeeper Snyk Node.js 答疑解惑错误代码片段123new Promise((resolve, reject) => { throw new Error('error')}).then(console.log) 没有 catch ,无法捕获异常更正如下:12345678new Promise((resolve, reject) => { throw new Error('error')}).then(console.log).catch(console.error)// 当你不知道哪个 promise 可能会隐藏问题时,使用 unhandledRejection ,可以打印出所有未处理的 Promise rejectprocess.on('unhandledRejection', (err) => { console.log(err)}) 错误代码片段123456function checkApiKey (apiKeyFromDb, apiKeyReceived) { if (apiKeyFromDb === apiKeyReceived) { return true } return false} 当你比较安全凭证时,至关重要的一点是不要泄露任何信息,因此你必须确保在固定时间内对其进行比较。如果你不这样做,你的应用程序将容易受到时间攻击。使用 cryptiles 模块:123function checkApiKey (apiKeyFromDb, apiKeyReceived) { return cryptiles.fixedTimeComparison(apiKeyFromDb, apiKeyReceived)} 以下代码输出结果是什么123456789Promise.resolve(1) .then((x) => x + 1) // 得到值 2 .then((x) => { throw new Error('My Error') }) .catch(() => 1) // 错误信息被抛弃,返回一个新的值 1 .then((x) => x + 1) // 得到值 2 .then((x) => console.log(x)) // 打印 2 .catch(console.error) // 没有异常抛出,这一行不执行// 最后输出 2]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>node.js</tag>
</tags>
</entry>
<entry>
<title><![CDATA[零配置打包工具——Parcel]]></title>
<url>%2F2017%2F12%2F27%2F%E6%96%B0%E6%8A%80%E6%9C%AF%2F%E9%9B%B6%E9%85%8D%E7%BD%AE%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7%E2%80%94%E2%80%94Parcel%2F</url>
<content type="text"><![CDATA[前言:Parcel 是零配置、极速的 web 应用打包工具。主要特点包括:快速打包,多核编译并且拥有文件系统缓存;原生支持打包JS、CSS、HTML、文件资源等;内置支持模块热替换 Parcel 是基于资源的,资源可以是任意文件。Parcel 会自动分析这些文件和包中引用的依赖。 Parcel 是一个约定大于配置的打包工具。 官网极速零配置Web应用打包工具——Parcel 安装NPM 1$ npm install -g parcel-bundler Yarn 1$ yarn global add parcel-bundler 运行Parcel可以以任何文件作为入口,但最好使用HTML或JS文件。 开发环境 1$ parcel index.html 生产环境 1$ parcel build index.html 如果使用了 Sass ,需要另外安装 node-sass ,使用 ES6、JSX 等,需要另外安装 babel ,并写好相应的 .babelrc 文件。 代码拆分Parcel 支持零配置代码拆分。通过使用动态 import() 函数来控制。与普通的 import 和 require 类似,但返回 Promise 对象,并且支持 async/await。 1234// page/about.jsexport function render () { // 渲染页面} 1234567import('./page/about').then((page) => { page.render()})// 或者使用 async/awaitconst page = await import('./page/about')page.render() 如何运作Parcel 会将资源树转换成文件束(bundle)树。打包流程有三个步骤: 构建资源树,资源会被解析,资源的依赖会被提取,资源会被转换成最终编译好的形态; 构建文件束(bundle)树,资源树被构建好,资源会被放置在文件束树中,一个入口资源会被构建成一个文件束(bundle),动态 import 会构建一个子文件束(bundle),这过程会引起代码拆分。如果资源被多于一个文件束(bundle)引用,它会被提升到文件束树中最近的公共祖先。 打包,文件束树构建成功后,每个文件束都会被 packager 写到一个特定文件类型的文件中,然后 packager 会自动从每个资源中将代码合并,生成到最终被浏览器加载的文件中 与其他打包工具的比对Parcel 与其他打包工具最大的不同就是零配置和速度快。零配置,这使得无论是新手还是老司机都能够很快把项目构建打包,同时在构建过程中速度要明显快于其他打包工具 browserify:22.98s webpack:20.71s parcel:9.98s parcel - with cache:2.64s 基于一个合理大小的应用,包含1726个模块, 6.5M 未压缩大小. 在一台有4个物理核心 CPU 的 2016 MacBook Pro 上构建。(以上数据来源于官网) 其他打包工具基本上是基于 JavaScript 资源,还有附加在其上的其它格式的资源。例如在 JS 文件中内联成字符串。 比如 webpack ,代码转译主要是靠 loader 进行字符串处理。一个 loader 处理完后,再将字符串传给下一个 loader,接着再进行 parse 为AST(抽象语法树),然后再 uglify(混淆代码) 和压缩。 Parcel 是先将代码转译成 AST,然后再进行转换,即便在某些场景下需要多次转译,然后 uglify ,最后也只需要 parse 一次。在这过程中,每一步都能够利用到已经解析过后的 AST,并且 Parcel 拥有多核处理和文件系统缓存,能够大大减少构建打包的速度。 总结总而言之,Parcel 简单易学,零配置和极速打包给开发者带来了更好的开发体验,但还是不够灵活,零配置相对的会增加更多约定,同时现在生态圈不完善。所以,现在 Parcel 应该比较适合于一些小型项目,或者是未应用前端框架的一些项目。当然我们还是要对 Parcel 保持关注,期待越来越好]]></content>
<categories>
<category>新技术</category>
</categories>
<tags>
<tag>新技术</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript Event Loop]]></title>
<url>%2F2017%2F12%2F22%2FJavaScript%2FJavaScript_Event_Loop%2F</url>
<content type="text"><![CDATA[单线程的JavaScript JavaScript 是单线程的,主要是因为 JavaScript 的主要用途是与用户交互,以及操作 DOM。如果 JavaScript 是多线程,那么当多个线程同时操作一个 DOM 元素时,那么就无法保证哪个线程优先去操作该 DOM 元素。 也许,有小伙伴会说:“我们可以通过HTML 5的 web worker 来创建 JavaScript 的多个线程啊”。是的,web worker 允许我们创建多个线程来执行 JavaScript,但创建的都是子线程,这些子线程都受到 JavaScript 唯一的主线程控制,而且这些子线程是不能够操作 DOM 元素的. 由于 JavaScript 的单线程机制,JavaScript代码是按照任务执行的,而任务分为同步任务和异步任务 同步任务:在主线程中执行,一个接着一个按顺序执行 异步任务:一开始就进入到一个任务队列中,主线程执行完当前任务后,就会去读取任务队列,某个异步任务可以开始了,该任务则会进入主线程执行 所以,JavaScript 执行的顺序是,先执行主线程上已有的任务,然后再读取“任务队列”,将“任务队列”中的任务加到主线程中继续执行。 任务队列,是一个事件队列,如果事件指定过回调函数,这些事件发生时就进入“任务队列”,等待主线程读取。我们常说的回调函数,也就是被主线程暂时挂起不执行的代码,当事件发生时,主线程才会将这代码加入进来执行。注意,同步任务总是在主线程读取“任务队列”之前执行的 事件循环(Event Loop)主线程读取“任务列表”这一过程是循环不断的,这种运行机制称为事件循环。 定时器任务队列,不仅仅存放了异步任务的事件,还存放了定时事件(setTimeout,setInterval)定时器 setTimeout 和 setInterval 只是把事件放置到任务队列中,必须等待任务队列中的事件执行完毕,主线程才会调用定时器相应的回调函数。 Microtasks 和 Macrotasks任务队列不止一个,还包括了 microtasks(微任务) 和 macrotasks(宏任务) microtasks process.nextTick promise Object.observe MutationObserver macrotasks setTimeout setInterval setImmediate I/O UI渲染 一个事件循环中会有一个或多个任务队列 任务队列就是宏任务(macrotasks) 每个事件循环都有一个 microtasks queue task queue == macrotasks queue != microtasks queue 主线程拥有一个执行栈和一个事件循环 因此,JavaScript 运行机制是这样的: 首先,全部代码的 script 是一个 macrotask。第一次事件循环,主线程先执行同步任务(其实也是macrotask),执行过程中会创造新的macrotask,接着继续执行,然后会把一些事件,如promise(发现 then 方法,则放入 microtask),放置在 microtask 中。如果有 microtask 则执行 microtask,没有则进入下一次循环。 总结为:同步完成,一个宏任务,所有微任务,一个宏任务,所有微任务… 最后,看以下这道面试题 123456789101112131415161718192021222324252627282930313233343536373839console.log('start')const interval = setInterval(() => { console.log('setInterval')}, 0)setTimeout(() => { console.log('setTimeout 1') Promise.resolve() .then(() => { console.log('promise 3') }) .then(() => { console.log('promise 4') }) .then(() => { setTimeout(() => { console.log('setTimeout 2') Promise.resolve() .then(() => { console.log('promise 5') }) .then(() => { console.log('promise 6') }) .then(() => { clearInterval(interval) }) }, 0) })}, 0)Promise.resolve() .then(() => { console.log('promise 1') }) .then(() => { console.log('promise 2') }) 输出: 1234567891011startpromise1promise2setIntervalsetTimeout1promise3promise4setIntervalsetTimeout2promise5promise6]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>JavaScript核心</tag>
</tags>
</entry>
<entry>
<title><![CDATA[文件断点续传(上传)以及秒传]]></title>
<url>%2F2017%2F12%2F10%2F%E7%A7%AF%E7%B4%AF%2F%E6%96%87%E4%BB%B6%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0%EF%BC%88%E4%B8%8A%E4%BC%A0%EF%BC%89%E4%BB%A5%E5%8F%8A%E7%A7%92%E4%BC%A0%2F</url>
<content type="text"><![CDATA[前言:文件断点续传,指的是在进行上传或者下载时,将文件分割成几块,每一块都单独上传或下载,如果某一块由于网络原因或者其他因素,导致上传或下载失败,那么可以从已经上传或下载的部分继续进行上传下载未完成的部分,节省时间,提高效率。此外,也支持暂停上传或下载。文件秒传,指的是当用户上传一个已被上传过(自己或他人上传到同一个文件服务器中)的文件时,文件能够直接跳过上传过程,达到秒传的效果。其中的原理就是,用户上传文件时,会同时发送文件的md5值,服务端会进行相应查找匹配,如果有相同的md5,则返回通知文件已上传过服务器,不必再次上传,实现秒传。本文主要讲断点续传之上传和秒传的实现 在 HTML5 普及之前,上传基本是通过flash实现的,断点续传的实现也相对麻烦,本篇就不涉及啦,下面进入主题。通过 HTML5 File API 进行文件分割,File 接口是基于 Blob 对象 Blob 对象表示不可变的类似文件对象的原始数据,包含两个属性 size (Blob对象中数据的大小)和 type (Blob对象包含数据的MIME类型)。还拥有一个方法 slice,返回一个新的 Blob对象,包含了源 Blob对象中指定范围内的数据。 对文件进行分割,使用的就是 File 继承于 Blob 对象的 slice 方法。12345678var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlicevar currentChunk = 0 // 当前文件块的编号var chunkSize = 2 * 1024 * 1024 // 每一块文件块的大小,一般设为2~5MBvar start = currentChunk * chunkSizevar end = (start + chunkSize > fileSize) ? fileSize : (start + chunkSize)blobSlice.call(file, start, end) 对文件分割完成后,则进行上传,上传时客户端和服务端都会保存当前文件块的编号。上传完成后,则进行下一块上传1234567891011121314151617181920212223242526272829303132333435function uploadData () { if (!fileSize) { alert('请选择要上传的文件') return } var xhr = new XMLHttpRequest() var chunkData = sliceChunks() // 分割文件,返回文件块数据 var formData = new FormData() fileIndex = currentChunk + 1 // 文件编号 if (fileIndex > chunks) return // 文件编号大于文件块总数时则终止上传 formData.append('fileName', file.name) formData.append('fileSize', file.size) formData.append('fileIndex', fileIndex) formData.append('fileContent', chunkData) // 文件块上传完成后,则继续上传下一块 xhr.upload.onload = function (e){ currentChunk++ if (fileIndex < chunks) { uploadData () } } // 显示上传进度 xhr.upload.onprogress = function (e) { uploadPrg.innerText = (fileIndex * 10000 / (chunks * 100)).toFixed(2) + '%' fileIndex === chunks && (uploadBtn.value = '上传完成') } xhr.open("POST", '/upload') xhr.send(formData) } 以上主要是前端需要实现,再来看下服务端(采用node.js实现)是如何将文件块接收,并拼接成完整文件的12345678910111213141516171819202122232425262728293031323334const http = require('http')const fs = require('fs')const express = require('express')const bodyParser = require('body-parser')var multipart = require('connect-multiparty') //在处理模块中引入第三方解析模块 var multipartMiddleware = multipart()var uploadPath = './upload'var app = express()var fileIndexapp.post('/upload', multipartMiddleware, (req, res, next) => { let data = req.body let fileData = req.files.fileContent // 上传目录是否存在,不存在则创建 if (!fs.existsSync(uploadPath)) { fs.mkdirSync(uploadPath) } if (!fileIndex || fileIndex < data.fileIndex) { fileIndex = parseInt(data.fileIndex) let fileChunk = fs.readFileSync(fileData.path) fs.appendFile('./upload/' + data.fileName, fileChunk) } else { fileIndex = '' fs.renameSync(fileData.path, './upload/' + data.fileName) } res.writeHead(200, {'Content-Type': 'text/plain'}) res.end()}) 在服务端实现过程中,主要的难点是如何接收前端传输的参数,前端参数都是通过 FormData 传到服务端,服务端需要对参数进行解析,这里采用 connect-multiparty 库进行解析参数。参数接收完成后,则进行文件的写入操作。 秒传的实现就较为简单了,主要是上传第一块文件块时,会将文件的md5值传到服务端,然后进行查找匹配。难点在于如何获取文件的md5值。获取文件的md5主要是使用 spark-md5 库。123456789101112import sparkMD5 from 'spark-md5'var fileReader = new FileReader()var spark = new sparkMD5.ArrayBuffer()// 读取文件内容fileReader.readAsArrayBuffer(file)fileReader.onload = function (e) { // 计算md5 spark.append(e.target.result) file.fileMD5 = spark.end()} 此外,对于超大文件(1G以上)需要做下优化处理,因为浏览器读取文件流的能力有限,如果一次性读取超大文件的文件流(FileReader),那么浏览器占用内存会瞬间飙升,系统会变得卡顿,甚至直接浏览器崩溃。 优化的方案是:将文件进行切割分块,一块一块地读取到文件流中,然后计算MD5值1234567891011121314151617181920212223242526272829303132333435363738import sparkMD5 from 'spark-md5'var chunkSize = 1024 * 1024 * 2, // 每一个文件块为2MB chunks = Math.ceil(file.size / chunkSize), // 文件块总数 chunk = 0, // 文件块的序号 spark = new sparkMD5.ArrayBuffer(), fileReader = new FileReader() function loadNext () { var start, end start = chunk * chunkSize end = Math.min(start + chunkSize, file.size) // 分块读取文件流 fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)) fileReader.onload = (e) => { spark.append(e.target.result) } fileReader.onloadend = () => { fileReader.onload = fileReader.onloadend = null if (++chunk < chunks) { setTimeout(loadNext, 1) } else { setTimeout(() => { file.fileMD5 = spark.end() loadNex = spark = null // 执行文件上传操作 uploadFile(file) }, 50) } }}loadNext() 源码:https://github.com/Peterlhx/html5-resume 如果对你有用,欢迎star~]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>项目总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[WebSocket基本原理和心跳机制]]></title>
<url>%2F2017%2F11%2F10%2F%E7%A7%AF%E7%B4%AF%2FWebSocket%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86%E5%92%8C%E5%BF%83%E8%B7%B3%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[WebSocket 基本概念WebSocket 是HTML5下一种新的计算机网络应用层的协议,是基于HTTP协议的,实现了客户端与服务端的全双工通信,客户端可以主动向服务端发起请求,服务端也可以主动向客户端推送数据。并且只需要第一次由客户端发起请求连接,连接成功后,客户端与服务端保持长久的连接,后续数据都以帧序列的形式传输。 单工通信,只支持数据传输在一个方向传输 半双工通信,允许数据在两个方向上传输,但同一时间只允许一个方向上的数据通信 全双工通信,允许数据在两个方向上同时传输 建立 WebSocket 连接客户端创建 WebSocket 对象(new WebSocket()),发起请求(ws://www.example.com/),与服务端进行 tcp 三次握手建立连接。请求报文:1234567GET /webfin/websocket/ HTTP/1.1Host: localhostUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==Origin: http://localhost:8080Sec-WebSocket-Version: 13 Upgrade: websocket 和 Connection: Upgrade 两个字段告知服务端,此次发起的是 Websocket 协议的请求。Sec-WebSocket-Key 则是客户端随机生成的,用于于服务端进行通信认证。响应报文:1234HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8= Sec-WebSocket-Accept 的值是服务端通过与客户端相同的密钥计算得到的与服务端建立连接后,则没有 HTTP 啥事了,后续将直接由 WebSocket 协议负责客户端与服务端之间的通信。 WebSocket 基本属性和方法构造函数 WebSocket(),客户端通过 new WebSocket(),传入 url 创建 WebSocket 实例。通过 onopen() 方法用于指定连接成功后的回调函数,onclose() 方法用于指定连接关闭后的回调函数,onmessage() 方法用于指定收到服务端数据后的回调函数,onerror() 方法用于指定连接失败后的回调函数 客户端实现1234567891011121314151617181920let ws = new WebSocket('ws://localhost:8080/')ws.onopen = function () { ws.send('Hello world')}ws.onclose = function (event) { console.log(event.code) // or reconnect...}ws.onmessage = function (event) { let data = event.data // handle data}ws.onerror = function (event) { // handle error event or reconnect} 服务端实现12345678let WebSocketServer = require('ws').Server,wss = new WebSocketServer({ port: 8080 });wss.on('connection', function (ws) { console.log('client connected'); ws.on('message', function (message) { console.log(message); });}); WebSocket 重连机制基本思路就是,在 WebSocket 连接关闭或者连接异常时,重新发起连接就可以了1234567ws.onclose = function (event) { reconnect()}ws.onerror = function (event) { reconnect()} 但是在使用 WebSocket 过程中,可能会出现网络断开的情况,比如信号不好,或者网络临时性关闭,这时候 WebSocket 的连接已经断开,而浏览器不会执行 WebSocket 的 onclose 方法,我们无法知道是否断开连接,也就无法进行重连操作。这时候需要另一种重连机制—— WebSocket 心跳重连机制1234567891011121314151617181920212223let timeout = 10000 // 与服务端断开后需要重连的时间let timer = nulllet heartConnect = { reset () { clearTimeout(timer) this.start() }, start () { timer = setTimeout(() => { ws.send('Heart beat') }, timeout) }}let ws = new WebSocket('ws://localhost:8080/')ws.onopen = function () { heartConnect.start()};ws.onmessage = function (event) { heartConnect.reset()} 心跳的基本思路是:通过定时器,当成功连接上时,开始计时,如果在定时器间隔内,收到服务端推送的消息,则重置定时器。如果超过60s还没收到服务端消息,则执行心跳检测,WebSocket 会向服务端发送消息,发送超时后,客户端会自动触发 onclose 方法,WebSocket 重连也就执行了。 WebSocket 的优劣势 优:只要建立一次连接,就可以连续不断的得到服务器推送的消息,节省带宽和服务器端的压力 劣:不兼容低版本浏览器 与Ajax 轮询、长轮询(long poll)比较 ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。 long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型,客户端发起连接后,如果没消息,就一直不返回 Response 给客户端。直到有消息才返回,返回完之后,客户端再次建立连接。 Ajax轮询需要服务器有很快的处理速度与快速响应。long poll 需要很高的并发,体现在同时容纳请求的能力]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>项目总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Vue 底层原理]]></title>
<url>%2F2017%2F09%2F10%2FVue%2FVue%20%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%2F</url>
<content type="text"><![CDATA[Vue 响应式原理Vue 最独特的特性之一,非侵入性的响应式系统。数据改变,视图会自动更新。 当 Vue 实例的 data 初始化时,Vue 将遍历 data 对象所有属性,并使用 Object.defineProperty 把这些属性转为 getter/setter 在 data 的属性被访问和修改时通知变化 每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把 data 的属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。 只有在 Vue 实例初始化时,就存在于 data 对象中的属性才能被主动转换为 getter/setter,这样属性才是响应的,如果需要动态添加属性,需要使用 Vue.set() 方法才能使得添加的属性是响应的 数组的响应,主要是复写了数组的proto,将该属性赋值于一个 overrideArrayProto,这个是经过转换过 getter/setter 的,进行监听过的 1234567891011121314151617181920212223242526272829function overrideArrayProto(array) { var originalProto = Array.prototype, overrideProto = Object.create(Array.prototype), self = this, result; Object.keys(OAM).forEach(function (key, index, array) { var method = OAM[index], oldArray = []; Object.defineProperty(overrideProto, method, { value: function () { oldArray = this.slice(0); var args = [].slice.apply(arguments); result = originalProto[method].apply(this, args); self.observe(this); // 进行 getter/setter 转换,设置监听 self._callback(this, oldArray); return result; }, writable: true, enumberable: true, configurable: true }); }, this); array.__proto__ = overrideProto;} 对象中存在的属性描述符主要有两种:数据描述符合存取描述符,数据描述符是一个具有值的属性;存取描述符是由 getter/setter 函数对描述的属性。存取描述符同时具有以下可选键值:get:该方法返回值被用作属性值;set:该方法将接受唯一参数,并将该参数的新值分配给该属性。 异步更新队列Vue 异步执行 DOM 更新,当数据变化时,Vue 将开启一个队列,并缓冲在同一事件循环中的所有数据改变,在下一个事件循环(tick)中,Vue 刷新队列并执行 DOM 更新,Vue 对异步队列尝试使用 Promie.then 和 MessageChannel,如果环境不支持,则使用 setTimeout(fn, 0) Vue 中,如果想在数据变化后,立即执行某些任务,可以在 Vue.nextTick(callback) 方法的回调函数里设置。 Vue 的Virtual DomVirtual Dom(虚拟 DOM),是指通过 JavaScript 模拟创建一棵 DOM 对象树,当实际的 DOM 需要更新时,先会更新这棵由 JavaScript 模拟的 DOM 对象树,然后再批量更新实际的 DOM 树。这样实现的原因,主要是操作 JavaScript 对象要远远快于直接操作 DOM,同时也避免了多次重排或重绘,这样能够极大的优化性能。 diff 算法Vue 监听到数据变化后,会自动更新视图。更新视图,在 Vue 内部会将 Virtual Dom 中的每一个 VNode 与之前一个旧的 VNode 对象进行 patch。 patch 就是将新老 VNode 节点进行比对,根据比较结果,最小程度的修改视图,减少实际的 DOM 操作。patch 的核心是 diff 算法。 diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。也就是说比较只会在同层级进行, 不会跨层级比较。 节点的比较: oldVNode === newVNode,该节点未发生改变 oldVNode.text !== newVNode.text,新旧节点存在 text,且 text 不相同,则会将实际的节点赋值为 newVNode.text oldCh && ch && oldCh !== ch,两个节点都有子节点,并且不相同,则继续比较子节点 只有新的节点有子节点,在实际 DOM 节点增加子节点 新节点没有子节点,老节点有子节点,则实际DOM 节点删除子节点 子节点的比较,diff 的核心 diff 完整过程图节点没有设置 key 节点设置了 key 节点设置 key 值后,能够让 Vue 尽可能复用节点。 参考:https://github.com/aooy/blog/issues/2]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>Vue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript匿名函数以及括号运算符]]></title>
<url>%2F2017%2F07%2F03%2FJavaScript%2FJavaScript%E5%8C%BF%E5%90%8D%E5%87%BD%E6%95%B0%E4%BB%A5%E5%8F%8A%E6%8B%AC%E5%8F%B7%E8%BF%90%E7%AE%97%E7%AC%A6%2F</url>
<content type="text"><![CDATA[先来看下这道面试题,写这篇博客也是由这道题引起的123456789101112131415161718192021var x = 20 var a = { x: 15, fn: function () { var x = 30 return function () { return this.x } } } console.log(a.fn()) console.log((a.fn())())console.log(a.fn()())console.log(a.fn()() == (a.fn())()) console.log(a.fn().call(this)) console.log(a.fn().call(a)) 如果你对这道题完全理解,没有任何疑惑,那么无须继续往下阅读了 匿名函数,意指没有名字的函数,常出现于对象方法、自执行函数、函数回调以及高阶函数(以函数为参数,或以函数为返回值)中,是函数的一种特殊形式。 从文字上可能不太好理解,直接看下面几个示例 对象方法将一个匿名函数赋值给对象的sayHello属性12345var obj = { sayHello: function () { console.log('Hello world!') }} 自执行函数通过括号运算符将匿名函数包括起来,转换成函数表达式,然后再通过括号运算符来调用匿名函数 当声明定义一个函数后,函数是一种对象,存储在堆内存中,函数名其实就是指向函数体的指针,当函数名后直接添加括号时,该函数将会被调用。12345678(function () { console.log('Hello world!')})()// 另一种写法,执行结果与上述没有差别(function () { console.log('Hello world!')}()) 注意:所有自执行函数都是全局的,也就是说自执行函数内部的this指针指向的是全局对象 window 。 函数回调在函数回调的情况中,回调的函数有两种,其一是函数有函数名,另外就是匿名函数。123456789// 回调函数有函数名$('#btn').click(function sayHello () { console.log('Hello world!')})// 回调函数是匿名函数$('#btn').click(function () { console.log('Hello world!')}) 高阶函数在高阶函数中,匿名函数常常被当做函数返回值,当返回一个匿名函数,匿名函数中又调用了它外层函数的变量时,就会形成闭包。 闭包,就是存在多个嵌套函数时,内层函数能够调用外层函数中的变量。12345678910var fn = function () { var outterWords = 'Hello outter' // console.log(innerWords) return function () { var innerWords = 'Hello inner' console.log(outterWords) }}console.log(fn()) // 'Hello outter' 在上述代码中,嵌套了两层函数,内层函数能够访问到外层函数作用域中的变量 outterWords ,但是外层函数不能访问内层函数作用域中的 innerWords ,这就是闭包的一种情形。闭包在JavaScript中是非常重要的,要讲的东西还有很多,这里不再展开,还不清楚的小伙伴可以上网查找相关资料。 这里推荐一个很赞的系列博客文章:深入理解JavaScript原型和闭包 回到高阶函数,上述代码中,其实也就是一个高阶函数,它将一个匿名函数当作返回值返回。 介绍完这几种匿名函数的形式,我们回到文章开头的那道面试题12345678910111213141516171819202122var x = 20 var a = { x: 15, fn: function () { var x = 30 return function () { return this.x } } } console.log(a.fn()) console.log((a.fn())())console.log(a.fn()())// a.fn()返回的值是函数表达式,类似于加了一层括号,然后再进行括号的调用,相当于自执行函数console.log(a.fn()() == (a.fn())()) console.log(a.fn().call(this)) console.log(a.fn().call(a)) 一步步解析:1console.log(a.fn()) 这部分相信大家都知道,打印的是 a.fn() 返回的匿名函数的函数体123function () { return this.x } 接着往下:1console.log((a.fn())()) 这部分我们首先要注意到,这里用到了两次括号运算符,第一次是将 a.fn() 返回的函数体转换为函数表达式,第二次则是调用这个函数表达式,也就是转换成了自执行函数,自执行函数的 this 指针指向的是全局对象 window 。所以输出结果为 20 1console.log(a.fn()()) 这部分我一开始也是比较困惑的,因为函数体后面直接加上括号运算符,是会报语法错误的,如下所示:1234function () { return this.x }()// Uncaught SyntaxError: Unexpected token ( 但是,当一个函数返回一个匿名函数,然后再加上括号运算符,则能够正常执行输出的。其实,当函数返回匿名函数时,返回的是函数表达式,函数表达式后加上括号运算符,则该函数会被调用,函数内部的 this 指向全局对象 window 。所以输出2012console.log(a.fn()())// 输出20 知道了前两个的输出,那么自然第三个也明白了,输出 true 。 1console.log(a.fn().call(this)) 这部分主要是 call 方法的运用。call 与 apply 方法的作用是更改函数调用的对象。上述代码中,this 指向的是全局变量 window ,调用a.fn()返回的匿名函数,输出的是全局的变量x: 20 。 1console.log(a.fn().call(a)) 这部分函数调用的对象为 a ,所以输出的是a内部的变量x: 15 。 扩展延伸:12345678910var scope = 'global scope';function checkScope () { var scope = 'local scope'; function f() { return scope; } return f;}checkScope()(); 可以看到这道题与上面那道题的第三个输出是有点相似,区别在于这道题返回的 scope 没有绑定在this指针上,而上面那道题是 this.x 。那么它们的输出结果是一样的吗?答案是否定的。12345678910var scope = 'global scope';function checkScope () { var scope = 'local scope'; function f() { return scope; } return f;}checkScope()(); // 输出'local scope' 区别就是在于有没有 this 指针。我们知道当一个函数返回一个函数体后,加上括号,会被当做函数表达式执行,函数表达式内部的 this 指针是指向全局变量的。如果没有 this 指针,那么执行返回的变量 scope ,首先会顺着作用域链去寻找,也就是外层函数中寻找,找到了 scope ,所以输出’local scope’。有 this 指针,则会在 this 指针指向的对象上去寻找 scope 变量,所以如果这道题是1return this.scope 输出的就是 ‘global scope’ 总结这篇文章讲述了匿名函数的几种应用场景:对象方法、自执行函数、函数回调以及高阶函数。最应该牢记的匿名函数的执行条件:将匿名函数通过 void , ~ , + ,- ,! ,() 等形式来转化为函数表达式,然后通过 () 调用。123void function () { console.log('Hello world!')}() 如果匿名函数没有任何关键字、运算符,那么就会报错。在一个函数中返回一个函数体,加上括号(),将变成函数表达式,如果这个函数表达式中返回一个普通变量,那么这个变量会在它的作用域链中去寻找;如果这个变量是绑定在 this 指针上,那么会在 this 指向的对象上寻找,如果在对象上找不到,则会顺着这个对象的原型链去寻找。]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>JavaScript核心</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript进阶技巧]]></title>
<url>%2F2017%2F06%2F26%2FJavaScript%2FJavaScript%E8%BF%9B%E9%98%B6%E6%8A%80%E5%B7%A7%2F</url>
<content type="text"><![CDATA[归纳总结了JavaScript中的一些常用的进阶编程技巧,包括惰性载入函数、函数柯里化、函数节流、函数防抖以及事件委托 惰性载入函数惰性载入表示函数执行的分支仅会发生一次。有以下两种实现惰性载入的方式 在函数被调用时再处理函数 在第一次调用过程中,该函数会被覆盖为另一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。 以创建XHR对象的createXHR()函数为例1234567891011121314151617181920212223242526272829function createXHR () { if (typeof XMLHttpRequest != 'undefined') { createXHR = function () { return new XMLHttpRequest(); }; } else if (typeof ActiveXObject != 'undefined') { createXHR = function () { if (typeof arguments.callee.activeXString != 'string') { var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'], i,len; for (i = 0, len = versions.length; i < len; i++) { try { new ActiveXObject(version[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex) { //... } } } return new ActiveXObject(arguments.callee.activeXString); }; } else { createXHR = function() { throw new Error('No XHR object available.'); }; } return createXHR;} 在这个惰性载入的 createXHR() 中,if语句的每一个分支都会为 createXHR 变量赋值,有效覆盖了原有的函数,最后一步就是调用新赋的函数,下次调用 createXHR() 时候,就会直接调用被分配的函数。 在声明函数时就指定适当的函数第一次调用函数时不会损失性能,而在代码首次加载时会损失一点性能12345678910111213141516171819202122232425262728var createXHR = (function createXHR () { if (typeof XMLHttpRequest != 'undefined') { return function(){ return new XMLHttpRequest(); }; } else if (typeof ActiveXObject != 'undefined') {v return function () { if(typeof arguments.callee.activeXString != 'string') { var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'], i,len; for (i = 0, len = versions.length; i < len; i++) { try { new ActiveXObject(version[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex) { //... } } } return new ActiveXObject(arguments.callee.activeXString); }; } else { return function () { throw new Error('No XHR object available.'); }; }})(); 这种方法的技巧是创建一个匿名、自执行的函数,用以确定应该使用哪一个函数实现。惰性载入函数的优点是只在执行分支代码时牺牲一点性能。 函数柯里化 与函数绑定紧密相关的主题是函数柯里化,它用于创建已经设置好了一个或多个参数的函数(在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。) 通俗点讲,函数柯里化就是把函数完全变成「接受一个参数;返回一个值」的固定形式。 函数柯里化的基本方法是:使用一个闭包返回一个函数。当函数被调用时,返回的函数还需要设置一些传入的参数。柯里化函数通常由以下步骤动态创建:调用一个函数并为它传入要柯里化的函数和必要的参数。123456789101112131415function curry (fn) { var args = Array.prototype.slice.call(arguments, 1); // 这个arguments是外部的,即curry的参数 return function () { var innerArgs = Array.prototype.slice.call(arguments); // 这个arguments是内部的,即curryAdd的参数 var finalArgs = args.concat(innerArgs); // 连接两个或多个数组 return fn.apply(null, finalArgs); // 没有考虑执行环境,所以调用apply时第一个参数是null };}function add(num1,num2){ return num1+num2;}var curriedAdd = curry(add, 5); // 返回已设置第一个参数为5的函数alert(curriedAdd(3)); // 8 结合函数柯里化的更复杂的bind()函数:12345678function curryBind (fn, context) { var args = Array.prototype.slice.call(arguments, 2); // 给被绑定的函数的参数是从第三个开始 return function () { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(context, finalArgs); };} 可传入任意参数的,不传参数时输出结果的柯里化函数:1234567891011121314151617181920212223242526var currying = function (fn) { var _args = []; return function () { // 参数为空时,计算结果 if (arguments.length === 0) { return fn.apply(this, _args); } // 将参数保存 Array.prototype.push.apply(_args, [].slice.call(arguments)); return arguments.callee; }}; var multi = function () { var total = 0; for (var i = 0, len = arguments.length; i < len; i++) { total += arguments[i]; } return total;}; var sum = currying(multi); sum(100,200)(300);sum(400);console.log(sum()); // 1000 函数柯里化依赖于闭包的特性,来保存中间过程中输入的参数,最后统一处理参数 函数节流 在指定时间间隔内只执行一次任务,避免在短时间内多次操作或调用函数,常见操作:滚动滚动条,触发scroll事件 函数节流的实践 《JavaScript高级程序设计》中的方法:123456function throttle (method , context) { clearTimeout(method.tId); method.tId = setTimeout(function () { method.call(context); }, 500);} 知友@大板栗的方法:1234567891011function throttle(fn, interval = 300) { let canRun = true; return function () { if (!canRun) return; canRun = false; setTimeout(() => { fn.apply(this, arguments); canRun = true; }, interval); };} 函数防抖的实践 任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。123456789function debounce (fn, interval = 300) { let timeout = null; return function () { clearTimeout(timeout); timeout = setTimeout(() => { fn.apply(this, arguments); }, interval); };} 事件委托 使用事件委托技术能让你避免对特定的每个节点添加事件监听器;相反,事件监听器是被添加到它们的父元素上。事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件。 通俗的讲,事件就是onclick,onmouseover,onmouseout等,委托呢,就是让别人来做,这个事件本来是加在某些元素上的,然而你却加到别人身上来做,完成这个事件。利用冒泡的原理,把事件加到父级上,触发执行效果。主要优点就是: 将批量元素监听的任务通过一个监听实现,减少DOM的事件绑定;监听动态生成子元素的事件触发。 这里要用到事件源:event 对象,事件源,不管在哪个事件中,只要你操作的那个元素就是事件源。ie:window.event.srcElement标准下:event.target123456789101112131415161718192021222324252627282930313233343536<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Document</title></head><body> <ul id="testUl"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> <input type="button" value="添加" id="addBtn"> <script> var testUl = document.getElementById('testUl'); var addBtn = document.getElementById('addBtn'); addBtn.onClick = function () { var li = document.createElement('li'); li.innerHtml = '6' testUl.appendChild(li) } testUl.onClick = function (event) { var event = event || window.event; var target = event.target || event.srcElement; if (target.nodeName.toLowerCase === 'li') { console.log(target.innerHtml) } } </script></body></html> 参考链接:浅析 JavaScript 中的 函数 currying 柯里化函数节流与函数防抖]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>编程技巧</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript异步编程]]></title>
<url>%2F2017%2F06%2F20%2FJavaScript%2FJavaScript%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B%2F</url>
<content type="text"><![CDATA[什么异步?异步,是相对于同步而言的。同步是指多个任务一个个按顺序去完成,异步则是将比较耗时的任务放到其他地方执行,接着执行当前任务列表中的任务,当耗时任务执行完毕,再将该任务的回调添加到当前任务列表中。举个栗子: 很多人去寄快递,然后快递员说:“排好队,一个个来”。人们就排好队,一个个跟快递员说明自己的需求,这就是同步。而异步呢,就是当排队寄快递的某个人,由于寄的东西比较多,也没整理好,快递员就跟他说:“你先把要寄送的东西整理下,给你个快递箱,整理好你告诉我”。然后那个人就到一旁整理自己寄送的物品,其他人继续跟快递员沟通,寄快递。当那个人整理好自己寄送的物品后,就跟快递员说:“整理好了”(回调)。快递员处理完手上的工作就立刻帮那个人寄快递,这就是异步。 异步,就是当任务A被执行,由于任务A需要执行相当长的时间,则把任务A放在其他地方(事件队列)执行,然后先执行后续的任务B,任务C,任务D等,当任务A执行完毕时,会将任务A的一个回调或多个回调插入到当前任务列表中,可能是任务B后面,任务C后面或任务D后面,这取决于任务A执行完毕的时间。 异步与同步的优缺点同步优点:更符合我们的思考模式,代码的先后顺序也是程序执行的先后顺序,确保一个任务接着一个任务执行,上一个任务未执行完成,下一个任务将不会开始执行。缺点:当某个任务耗时很长时,后面的任务必须排队等着,会拖延整个程序的执行,常见的就是浏览器卡死。 异步优点:当某个任务被“调用”后不会等它执行结束再执行它后面的代码,而是调用之后直接往下执行,异步函数的“执行”实际上是放在“其他地方”,待“执行”完成后再把结果通过回调函数来进行进一步的使用或处理,这样也就不会导致程序卡死。缺点:异步编程的代码可读性较差(通过近些年的一些方法,可以像写同步代码一样实现异步),而且在早期的编码风格中,异步经常导致回调地狱(不断的回调嵌套)。 异步编程的六种方法回调函数回调函数是异步编程最简单,也是最原始的方法。1234567891011var callback = function () { console.log('This is a callback function')}var fn = function (callback) { setTimeout(() => { callback() }, 1000)}fn(callback) 上述代码中,callback需要等待fn执行后的结果,所以将callback传入到fn中,在fn执行完成后,立即调用callback。这看上去很简单,但是在很多业务场景中,可能会很复杂,例如任务B等待任务A执行的结果,任务C等待任务B执行的结果。。。可以得到以下代码:1234567setTimeout(function A () { setTimeout(function B () { setTimeout(function C() { // ... }) })}) 这样就会导致回调地狱,不仅代码耦合性高,流程混乱,而且阅读性差。 事件监听事件监听,也就是一种事件驱动的模式,任务的执行不取决于代码顺序,而是取决于事件触发的时刻。下面是一个常见的栗子(jQuery写法):1$('#test').on('click', fn) 上述代码中,当元素被点击后,立即执行fn函数,这也是一种异步编程的形式。这种方式在日常中用得也多,也很方便,容易理解。但如果大量使用的话,那么整个程序将变成事件驱动模式,运行流程不太清晰 发布/订阅模式发布/订阅模式,其实与事件监听有点类似,但优于事件监听模式。发布/订阅模式,会有个消息中心,当一个任务完成时,会向消息中心发送一个“已执行完毕”的消息,其他任何订阅了这个消息的任务就会开始执行1234567msgCenter.subscribe('sendMsg', f2)function f1 () { setTimeout(() => { msgCenter.publish('sendMsg') })} 上述代码中,任务f2向消息中心订阅了’sendMsg’的消息,而当f1执行后,会发送’sendMsg’消息,f2收到’sendMsg’消息后,立即开始执行。 发布/订阅模式能够通过消息中心很好地管理消息与消息订阅者,但是此方法需要借助第三方库,或者自主实现,相对较复杂。 Promise 对象Promise 对象是一个具有then方法的对象,同时也被称为thenable对象。在Promise编程中,每一个异步任务都会返回一个Promise对象,该对象具有then方法,允许指定回调函数。Promise 对象只有三种状态:pending、fulfilled、rejected,三种状态中,只能由pending转换为fulfilled或rejected,此过程不可逆,同时fulfilled与rejected也不可以相互转换。 ES6将Promise纳入了标准,不再需要第三方库来实现Promise,下面介绍Promise在ES6中的运用:1234567891011121314// MDN示例var myFirstPromise = new Promise(function(resolve, reject){ //当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...) //在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法. setTimeout(function(){ resolve("成功!"); //代码正常执行! }, 250);});myFirstPromise.then(function(successMessage){ //successMessage的值是上面调用resolve(...)方法传入的值. //successMessage参数不一定非要是字符串类型,这里只是举个例子 console.log("Yay! " + successMessage);}); 上述就是ES6中Promise的运用,这只是简单的例子,在MDN中还有更多关于Promise的介绍和用法。 Generator 生成器Generator 生成器,这是ES6中的新特性。 生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。下面结合代码来理解下:12345678910 function* getNum() { yield 1; yield 2; yield 3; }//调用生成器,生成一个可迭代的对象 const gen = getNum(); gen.next(); // {value: 1, done: false} gen.next(); // {value: 2, done: false} gen.next(); // {value: 3, done: true} 生成器在形式上与普通函数差别不大,主要使用 function* 的形式,同时在生成器中还拥有 yield 关键字。生成器最大的特点是:当代码执行后,遇到 yield 关键字时会暂停,当调用生成器生成的对象的next()方法时,就继续执行。这也就是说,生成器可以将函数的执行权交出。 接下来看下,生成器 Generator如何实现异步编程:12345678910111213141516171819202122232425// 代码中的定时器用于模拟异步请求,可替换成相应的ajax代码 function getFirstName () { setTimeout(() => { gen.next('hello'); },2000); } function getLastName () { setTimeout(() => { gen.next('world'); },1000); } function* getFullName () { let firstName = yield getFirstName() let lastName = yield getLastName() console.log(firstName + lastName) } var gen = getFullName() gen.next() // 立即打印:{value: undefined, done: false} // 几秒后打印:helloworld 可以看到,上述代码代码能够让我们像书写同步代码一样写异步代码,而且不用再写回调函数。这是一种非常不错的异步编程的方式。 async/await这是ES7中提出异步函数,返回一个Promise对象。async/await其实是对Promise和Generator的封装,也就是一种语法糖。123456789async function add(x) { var a = await resolveAfter2Seconds(20); var b = await resolveAfter2Seconds(30); return x + a + b;}add(10).then(v => { console.log(v); // 4s后输出60}); 异步函数可能会包括 await 表达式,这将会使异步函数暂停执行并等待 promise 解析传值后,继续执行异步函数并返回解析值。也就是说,使用 async 声明的函数,如果函数体内有 await 关键字,那么将等待 await 后的函数传回Promise对象,才继续往下执行。 async/await 形式的异步编程,具有更好的语义,更广的适用性(async函数的await命令后面,可以是Promise对象和原始类型的值)。这也是目前异步编程最好的一种形式。 参考链接:阮一峰:Javascript异步编程的4种方法Promise In ES6前端的异步解决方案]]></content>
<categories>
<category>积累</category>
</categories>
<tags>
<tag>JavaScript核心</tag>
</tags>
</entry>
<entry>
<title><![CDATA[7-days-nodejs读书笔记]]></title>
<url>%2F2017%2F06%2F13%2FNode%2F7-days-nodejs%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[前言,本书书名有点夸张,任何一门编程语言都无法在短期内学会,必须坚持不懈进行学习,才能够学有所成。但这本书是挺适合入门的,通过这本书能够了解到node.js的一些基本知识,了解node.js能够实现什么场景下的业务。同时推荐另一本node.js入门书籍,名字就叫做《node.js入门》(The Node Beginner Book)。基本入门之后,那么必须要拜读下朴灵大神的著作《深入浅出Node.js》,这是进阶学习node.js的必读之书! 模块require函数用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对象exports对象是当前模块的导出对象,用于导出模块公有方法和属性。123exports.hello = function () { console.log('Hello World!');}; 通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。 模块路径解析规则require函数支持斜杠(/)或盘符(C:)开头的绝对路径,也支持./开头的相对路径。require函数支持第三种形式的路径,写法类似于foo/bar,并依次按照以下规则解析路径,直到找到模块位置: /home/user/node_modules/foo/bar/home/node_modules/foo/bar/node_modules/foo/bar 包JS模块的基本单位是单个JS文件,但复杂些的模块往往由多个子模块组成。为了便于管理和使用,我们可以把由多个子模块组成的大模块称做包,并把所有子模块放在同一个目录里。在组成一个包的所有子模块中,需要有一个入口模块,入口模块的导出对象被作为包的导出对象。 当入口模块的文件名为index.js时,加载模块可以使用模块所在目录的路径代替模块文件路径:12var cat = require('/home/user/lib/cat');var cat = require('/home/user/lib/cat/index'); 以上两条语句等价。因此采用第一种写法,感觉整个目录被当做单个 模块使用,更有整体感。 若想自定义入口模块的文件名和存放位置,可以在包目录下包含一个 package.json 文件,在其中配置入口模块的路径:1234{ "name":"cat", "main":"./lib/main.js"} 这样便可以使用require(‘home/lib/user/cat’)加载模块。 小文件拷贝,直接使用fs的读写方法:readFileSync()和writeFileSync()大文件拷贝,考虑到内存有限,因此应采取流的方法进行读写,读一点,写一点,使用fs的可读流和可写流:createReadStream()和createWriteStream() Buffer与字符串有一个重要区别。字符串是只读的,并且对字符串的任何修改得到的都是一个新字符串,原字符串保持不变。至于Buffer,更像是可以做指针操作的C语言数组。 文件系统fs模块提供的API基本上可以分为以下三类:文件属性读写:常用的有fs.stat、fs.chmod、fs.chown等 文件内容读写:fs.readFile、fs.readdir、fs.writeFile、fs.close等 底层文件操作:fs.open、fs.read、fs.write、fs.close等 nodeJS最精华的异步IO模型在fs模块里有着充分的体现,例如通过回调函数传递结果:1234567fs.readFile(pathname,function(err,data){ if(err){ //deal with error }else{ //deal with data }}) 此外,fs模块的所有异步API都有对应的同步版本,同步API除了方法名的末尾多了一个Sync之外,异常对象与执行结果的传递方式也有相应变化。123456try{ var data = fs.readFileSynv(pathname) //deal with data}catch(err){ //deal with error} PathnodeJS提供了path内置模块来简化路径相关操作,并提高代码可读性。常用API:path.normalize将传入的路径转换为标准路径,具体讲,就是除了解析路径中的.和..外,还能去除多余的斜杠。如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用该方法保证路径的唯一性。12345678910var cache = {}function store(key,value){ cache[path.normalize(key)] = value}store('foo/bar',1)store('foo//baz//../bar',2)console.log(cache) 注意:标准化之后的路径里的斜杠在Windows系统下是\,而在Linux系统下是/。如果想保证任何系统下都使用/作为路径分隔符的话,需要用.replace(/\/g, ‘/‘)再替换一下标准路径。 path.join将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系统下正确使用相应的路径分隔符。1path.join('foo/','baz/','../bar').replace(/\\/g, '/') path.extname当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用。1path.extname('foo/bar.js') 遍历目录递归算法遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法通过不断缩小问题的规模来解决问题。1234567function factorial(n){ if(n === 1){ return 1 }else{ return n*factorial(n-1) }} 注意:使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。 遍历算法目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。深度优先,意味着到达一个节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是A > B > D > E > C > F。 A / \ B C / \ \ D E F 同步遍历1234567891011function travel(dir,callback){ fs.readdirSync(dir).forEach(function(file){ var pathname = path.join(dir,file) if(fs.statSync(pathname).isDirectory()){ travel(pathname,callback) }else{ callback(pathname) } })} 异步遍历如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理相同。12345678910111213141516171819function travel(dir,callback,finish){ fs.readdir(dir,function(err,files){ (function(i){ if(i < files.length){ var pathname = path.join(dir,files[i]) fs.stat(pathname,function(err,stats){ if(stats.isDirectory()){ travel(pathname,callback,function(){ next(i+1) }) } }) }else{ finish && finish() } })(0) })} 文件编码BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符(‘\uFEFF’),位于文本文件头部。可通过BOM标记来判断文件的编码格式,但在读取文件时,如果不去掉BOM,则在某些情况下会有问题,如合并几个JS文件,若中间有BOM标记,则会导致报错。BOM的移除:123456789function readText(pathname){ var bin = fs.readFileSync(pathname) if(bin[0] === 0xEF && bin[1] === 0xBB &&0xBF){ bin = bin.slice(3) } return bin.toString('utf-8')} 网络操作HTTPhttp模块提供两种使用方式:作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应;作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。 HTTP请求本质上是一个数据流,由请求头和请求体组成。HTTP响应本质上也是一个数据流,由响应头和响应体组成。 request对象可以当作一个只读数据流来访问请求数据;response对象可以当作一个只写流来写入响应数据。 URLparse()方法将一个URL字符串解析为URL对象;format()方法允许将一个URL对象转换为URL字符串;resolve()方法用于拼接URL。 Query Stringquerystring模块用于实现URL参数字符串与参数对象的互相转换。querystring.parse()将URL参数字符串转换为URL参数对象;querystring.stringify()将URL参数对象转换为URL参数字符串. Zlibzlib模块提供了数据压缩和解压功能。zlib.gzip()压缩数据;zlib.gunzip()解压数据; Netnet模块可用于创建Socket服务器或Socket客户端。 进程管理NodeJS可以创建子进程并与其协同工作,把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用。 processprocess不是内置模块,是一个全局对象,可以感知和控制NodeJS自身进程的方方面面。 Child Processchild_process模块可以创建和控制子进程。 Clustercluster模块是对child_process模块的进一步封装,专用于解决单进程NodeJS web服务器无法充分利用多核CPU的问题。 应用场景:获取命令行参数通过process.argv获取命令行参数,但node执行程序路径和主模块文件路径固定占据了argv[0]和argv[1]两个位置,而第一个命令行参数从argv[2]开始。 如何退出程序12345try{ //...}catch(err){ process.exit(1);} 如何控制输入输出标准输入流(stdin)、一个标准输出流(stdout)、一个标准错误流(stderr):process.stdin、process.stdout和process.stderr,第一个是只读流,后两个是只写流。 如何降权在Linux系统下,我们知道需要使用root权限才能监听1024以下端口。但是一旦完成端口监听后,继续让程序运行在root权限下存在安全隐患,因此最好能把权限降下来。12345678http.createServer(callback).listen(80,function(){ var env = process.env, uid = parseInt(env['SUDO_UID'] || process.getuid(), 10), gid = parseInt(env['SUDO_GID'] || process.getgid(), 10) process.setgid(gid) process.setuid(uid)}) Tips: 如果是通过sudo获取root权限的,运行程序的用户的UID和GID保存在环境变量SUDO_UID和SUDO_GID里边。如果是通过chmod +s方式获取root权限的,运行程序的用户的UID和GID可直接通过process.getuid和process.getgid方法获取。 process.setuid和process.setgid方法只接受number类型的参数。 降权时必须先降GID再降UID,否则顺序反过来的话就没权限更改程序的GID了。 如何创建子进程1234567891011121314var child_process = require('child_process')var child = child_process.spawn('node',['net_server.js'])child.stdout.on('data',function(data){ console.log('stdout' + data)})child.stderr.on('data',function(data){ console.log('stderr:' + data)})child.on('close',function(code){ console.log('child process exited with code ' + code)}) spawn(exec, args, options)方法,该方法支持三个参数。第一个参数是执行文件路径,可以是执行文件的相对或绝对路径,也可以是根据PATH环境变量能找到的执行文件名。第二个参数中,数组中的每个成员都按顺序对应一个命令行参数。第三个参数可选,用于配置子进程的执行环境与行为。 进程间如何通讯在Linux系统下,进程之间可以通过信号互相通信。1234567891011/* parent.js */var child_process = require('child_process')var child = child_process.spawn('node',['child.js'])child.kill('SIGTERM')/* child.js */process.on('SIGTERM',function(){ cleanUp() process.exit(0)}) kill方法本质上是用来给进程发送信号的,并不是关闭进程。进程收到信号后具体要做啥,完全取决于信号的种类和进程自身的代码。 如何守护子进程守护进程一般用于监控工作进程的运行状态,在工作进程不正常退出时重启工程进程,保障工程进程不间断运行。监听‘exit’事件,当退出状态码不等于0时,重新启用进程。1234567891011function spawn(mainModule){ var worker = child_process.spawn('node',[mainModule]) worker.on('exit',function(code){ if(code !== 0){ spawn(mainModule); } })}spawn('worker.js') 异步编程NodeJS最大的卖点——事件机制和异步IO。而异步编程是NodeJS最大的特点,没有掌握异步编程就不能说是真正学会了NodeJS。 回调异步编程的直接体现就是回调。JS是单线程运行,这决定了JS在执行完一段代码之前无法执行包括回调函数在内的别的代码。也就是说,在平行线程完成工作了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程有空闲时才能开始执行。 代码设计模式1、函数返回值同步方式下:1var output = fn1(fn2('input')) 而在异步方式下,由于函数执行结果不是通过返回值,而是通过回调函数传递:12345fn2('input',function(output2){ fn1(output2,function(output1){ })}) 多个回调嵌套,代码结构会比较混乱,阅读性差。 2、遍历数组 3、异常处理JS自身提供的异常捕获和处理机制——try..catch..,只能用于同步执行的代码。 4、域NodeJS提供了domain模块,可以简化异步代码的异常处理。域的概念:一个域就是一个JS运行环境,在一个运行环境中,如果一个异常没有被捕获,将作为一个全局异常被抛出。NodeJS通过process对象提供了捕获全局异常的方法。123process.on('uncaughtException',function(err){ console.log('Error: %s',err.message)}) 域的示例:123456789101112131415161718192021222324252627282930var http = require('http')function async(request,callback){ asyncA(request,function(data){ asyncB(request,function(data){ asyncC(request,function(data){ callback(data) }) }) })}http.createServer(function(request,response){ var d = domain.create() d.on('error',function(){ response.writeHead(500) response.end() }) d.run(function(){ async(request,function(data){ response.writeHead(200) response.end(data) }) })}) 通过create()方法创建子域对象,并通过run()方法进入需要在子域中运行的代码的入口点。 陷阱无论是通过process对象的uncaughtException事件捕获到全局异常,还是通过子域对象的error事件捕获到了子域异常,在NodeJS官方文档中都强烈建议处理完异常后立即重启程序,而不是让程序继续运行。 总结 本书讲得相对浅显易懂,能够让新手快速入门node.js,基本了解node.js,但要想学好,并且更好地应用于实践中,还需要继续学习专研,再次推荐下《深入浅出Node.js》!]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>node.js</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Mac终端环境配置-iTerm2]]></title>
<url>%2F2017%2F06%2F13%2F%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA%2FMac%E7%BB%88%E7%AB%AF%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE-iTerm2%2F</url>
<content type="text"><![CDATA[工欲善其事,必先利其器 一个便利的开发工具,往往能够让我们事半功倍,极大提升我们的开发效率,同时也能够让我们更愉悦地写代码😃 首先应该安装homebrew,homebrew是Mac OSX中非常好用的一个软件包管理工具,被誉为Mac OSX中失传已久的包管理器。安装很简单:1ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 装完包管理器,接着安装iTerm2,iTerm2是Mac上最好用的终端,没有之一。1brew install iTerm2 接着安装git,然后安装oh-my-zsh12git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh /* 下载oh-my-zsh */cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc /* 替换zshrc文件 */ 安装完这些后,我们就可以更改iTerm2的配色,一种好的配色方案能够提升用户体验,提高工作效率!这里采用solarizedSolarized 的主页:http://ethanschoonover.com/solarized 可以直接到solarized的主页下载或者使用git下载:1git clone git://github.com/altercation/solarized.git 下载完成后,打开solarized/iterm2-colors-solarized,双击里面的文件即可将配色方案添加到iTerm2的方案列表中。然后打开iTerm2,点击mac顶部菜单中iTerm2,选择Preferences->Profiles->colors,然后在面板右下角Color Presets中选择Solarized Dark 或者 Solarized Light。同时在当前这个面板可以设置iTerm2窗口的其他一些颜色这样配置完成,重新打开iTerm2后,窗口可能会变成灰蒙蒙的,或者打开vim时会出现这情况。此时,应该点击mac顶部菜单中iTerm2,选择Preferences->Profiles->Text,将Text Rendering中的Draw bold text in bright colors 选项取消勾选。 接着,iTerm2有个比较酷炫的功能,就是可以调整终端窗口的透明度,设置透明度后,逼格再次提升😃步骤:点击mac顶部菜单中iTerm2,选择Preferences->Profiles->window,调整Transparency最后重启下iTerm2,就可以看到我们酷炫的终端环境啦!]]></content>
<categories>
<category>开发环境</category>
</categories>
<tags>
<tag>Mac</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript设计模式(工厂模式)]]></title>
<url>%2F2017%2F05%2F27%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%2FJavaScript%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F%EF%BC%89%2F</url>
<content type="text"><![CDATA[简单工厂模式 创建型模式,又叫做静态工厂方法模式,不属于23种GOF设计模式。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。实例拥有共同的父类。 123456789101112function Person(name, age, sex) { var obj = {}; obj.name = name; obj.age = age; obj.sex = sex; obj.sayHello = function() { console.log('My name is' + this.name); }}var person1 = new Person('Peter', 23, 'male');var person2 = new Person('Helen', 22, 'female'); 工厂方法模式 定义创建各类对象的接口,但是让子类决定实例化哪个类。工厂方法将类的实例化延迟到子类。本质上就是根据不同的输入,创建出不同类型对象。 123456789101112131415161718192021function Computer(type) { let computer; if (type === 'Macbook') { computer = new Mackbook(); } else if (type === 'Surface') { computer = new Surface(); }}function Macbook() { this.price = 10000; this.os = 'OSX';}function Surface() { this.price = 9999; this.os = 'windows';}var computer1 = Computer('Macbook');var computer2 = Computer('Surface');]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript设计模式(单例)]]></title>
<url>%2F2017%2F05%2F24%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%2FJavaScript%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E5%8D%95%E4%BE%8B%EF%BC%89%2F</url>
<content type="text"><![CDATA[单例模式 单例模式,又称单体模式,定义为产生一个类的唯一实例。运用单例模式,能够很好的将代码划分为一个个命名空间,同时也减少了变量对全局命名空间的污染。通常是基于某个功能模块,创建一个单例对象,这样能够良好的将该功能模块代码与其他功能模块代码隔离 最常见,最简单的的单例模式就是对象字面量1234567var singleton = { attribute1: 'hello', attribute2: 'world', method: function () { console.log('This is a singleton example') }} 通过只将一个对象变量暴露在全局变量中,其余的属性和方法都添加在这对象上,减少了变量对全局变量的污染,同时也将代码划分到一个个不同的命名空间中。此外,还可以与闭包结合,实现拥有私有变量的单例对象:1234567891011121314151617181920var Singleton = (function () { var instance; function initInstance () { var object = new Object(); object.msg = "Singleton instance"; return object; } return { getInstance: function () { if(!instance) { instance = initInstance(); } return instance; } };})();var instance1 = Singleton.getInstance();var instance2 = Singleton.getInstance();instance1 === instance2; // true 上述产生的实例都是同一个。 在日常,我更喜欢这样写单例模式:12345678910111213141516171819202122232425262728(function() { var singleton, _self; singleton = _self = { // 数据 data: { url: 'someurl' }, // 原素 elem: { someElem: document.getElementById('#someElem'), jQueryElem: $('#jQueryElem') }, bind: function () { _self.elem.someElem.onClick = function() { } }, fns: { method: function () { console.log(_self.data.url) } }, init: function () { _self.bind(); } } singleton.init();})();]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[《高性能JavaScript》读书笔记(三)]]></title>
<url>%2F2017%2F05%2F22%2F%E7%AC%94%E8%AE%B0%2F%E3%80%8A%E9%AB%98%E6%80%A7%E8%83%BDJavaScript%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89%2F</url>
<content type="text"><![CDATA[第七章 Ajax 异步JavaScript和XMLAjax可以通过延迟下载大量资源使页面加载更快。它通过在客户端和服务器之间异步传送数据,避免页面集体加载。还用于在一次HTTP请求中获取整个页面的资源。 数据传输Ajax,在它最基本的层面,是一种与服务器通讯而不重载当前页面的方法,数据可从服务器获得或发送给服务器。 请求数据有以下五种常用技术用于向服务器请求数据: XMLHttpRequest(XHR);动态脚本标签插入;iframes;Comet;多部分的XHR。 XMLHttpRequest目前最常用的方法中,XMLHttpRequest(XHR)用来异步收发数据。所有现代浏览器都能够很好地支持它,而且能够精细地控制发送请求和数据接收。你可以向请求报文中添加任意的头信息和参数(包括GET和POST),并读取从服务器返回的头信息,以及响应文本自身。12345678910111213141516var url ='/data.php';var params = [ 'id=934875', 'limit=20']; var req = new XMLHttpRequest();req.onreadystatechange = function(){ if(red.readyState === 4){ //表示整个响应已经接收完并可用于操作 var responseHeaders = req.getAllRequestHeaders(); var data = req.responseText; }}req.open('GET',url+'?'+params.join('&',true));req.setRequestHeader('X-Requested-With','XMLHttpRequest');req.send(null); 根据readyState的属性值来判别当前响应的状态;readyState等于4表示接收完并可用于操作;等于3时表示此时正在与服务器交互,响应报文还在传输中。这就是所谓的“流”,它是提高数据请求性能的强大工具。老版本的 IE 也不提供 readyState 3,它不支持流。从请求返回的数据像一个字符串或者一个 XML 对象那样对待,这意味着处理大量数据将相当缓慢。 使用XHR时,应使用POST还是GET如果 请求不改变服务器状态 只是取回数据(又称作 幂等动作)则使用GET操作。GET请求被缓存起来,如果你多次提取相同的数据可提高性能。 只有当URL和参数长度超过了2048个字符时才使用POST提取数据。因为IE限制了URL的长度,过长将导致请求(参数)被截断。 动态脚本标签插入动态脚本标签插入这以技术克服了XHR的最大限制:它可以从不同域的服务器上获取数据。123var script = document.createElement("script");script.src = "http://any-domain.com/js/index.js";document.getElementsByTagName("head")[0].appendChild(script); 动态脚本标签插入与XHR相比只提供更少的控制。 你不能通过请求发送信息头。 参数只能通过GET方法传递,不能用POST。你必须等待所有数据返回之后才可以访问它们。你不能访问响应信息头或者像访问字符串那样访问整个响应报文。 因为响应报文 被用作脚本标签的源码,它必须是可执行的JavaScript。你不能使用裸XML,或裸JSON,任何数据,无论什么格式,必选在一个回调函数之中被组装起来。 其响应结果是运行JavaScript。 请小心使用这种技术从你不能直接控制的服务器上请求数据。JavaScript 没有权限或访问控制的概念,所以你的页面上任何使用动态脚本标签插入的代码都可以完全控制整个页面。包括修改任何内容、将用户重定向到另一个站点,或跟踪他们在页面上的操作并将数据发送给第三方。使用外部来源的代码时务必非常小心。 多部分XHR多部分XHR允许你一个HTTP请求就可以从服务器端获取多个资源。它通过将资源(可以是CSS文件,HTML片段,JavaScript代码或base64编码的图片)打包成一个由特定分隔符界定的大字符串,从服务器端发送到客户端。 让我们从头到尾跟随这个过程。首先,发送一个请求向服务器索取几个图像资源:123456789101112131415161718192021222324252627282930var req = new XMLHttpRequest(); req.open('GET','rollup_images.php',true);req.onreadystatechange = function(){ if(req.readyState == 4){ splitImages(req.responseText); }};req.send(null);//rollup_images.php$images = array('kitten.jpg', 'sunset.jpg', 'baby.jpg');foreach ($images as $image) {$image_fh = fopen($image, 'r');$image_data = fread($image_fh, filesize($image));fclose($image_fh);$payloads[] = base64_encode($image_data);}$newline = chr(1); echo implode($newline, $payloads);//splitImages()function splitImages(imageString) { var imageData = imageString.split("\u0001"); var imageElement; for (var i = 0, len = imageData.length; i < len; i++) { imageElement = document.createElement('img'); imageElement.src = 'data:image/jpeg;base64,' + imageData[i]; document.getElementById('container').appendChild(imageElement); }} 上述代码中,图像不是从 base64 转换成二进制,而是使用 data:URL 并指定 image/jpeg 媒体类型。 下面的函数用于将 JavaScript 代码、CSS 样式表和图片转换为浏览器可用的资源:12345678910111213141516function handleImageData(data, mimeType) { var img = document.createElement('img'); img.src = 'data:' + mimeType + ';base64,' + data; return img;}function handleCss(data) { var style = document.createElement('style'); style.type = 'text/css'; var node = document.createTextNode(data); style.appendChild(node); document.getElementsByTagName_r('head')[0].appendChild(style);}function handleJavaScript(data) { (data);} 由于MXHR响应报文越来越大,有必要在每个资源收到时立即处理,而不是等待整个响应报文接收完成。通过监听readyState3实现:123456789101112131415161718192021222324var req = new XMLHttpRequest();var getLatestPacketInterval, lastLength = 0;req.open('GET', 'rollup_images.php', true);req.onreadystatechange = readyStateHandler;req.send(null);function readyStateHandler(){ if(req.readyState === 3 &&getLatestPacketInterval === null){ getLatestPacketInterval = window.setInterval(function(){ getLatestPacket(); },15); } if(req.readyState === 4){ clearInterval(getLatestPacketInterval); getLatestPacket(); }}function getLatestPacket(){ var length = req.responseText.length; var packet = req.responseText.substring(lastLeng,length); processPacket(packet); lastLength = length;} 当 readyState 3 第一次发出时,启动了一个定时器。每隔 15 毫秒检查一次响应报文中的新数据。数据片段被收集起来直到发现一个分隔符,然后一切都作为一个完整的资源处理。 使用MXHR的最大缺点是:以此方法 获得的资源不能被浏览器缓存。此外,在IE7及其更低版本不支持readyState或data:URL。 尽管有这些缺点,但某些情况下 MXHR 仍然显著提高了整体页面的性能:网页包含许多其他地方不会用到的资源(所以不需要缓存),尤其是图片;网站为每个页面使用了独一无二的打包的JavaScript或CSS文件以减少HTTP请求,因为它们对每个页面都是独一无二的,所以不需要从缓存中读取,除非重新载入特定页面。 由于HTTP请求是Ajax中最极端的瓶颈之一,减少其需求数量对整个页面性能有很大影响。 发送数据当数据只需发送服务器时,有两种广泛应用的技术:XHR和灯标。 XMLHttpRequestXHR主要用于从服务器获取数据, 它也可以用来将数据发回。 数据可以用 GET 或 POST 方式发回,以及任意数量的 HTTP 信息头。当你向服务器发回的数据量超过浏览器的最大URL长度时XHR特别有用。12345678910111213141516171819202122var url = '/data.php';var params = [ 'id=934875', 'limit=20'];var req = new XMLHttpRequest();req.onerror = function() { setTimeout(function() { xhrPost(url, params, callback); }, 1000);};req.onreadystatechange = function() { if (req.readyState == 4) { if (callback && typeof callback === 'function') { callback(); } }};req.open('POST', url, true);req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');req.setRequestHeader('Content-Length', params.length);req.send(params.join('&')); 当使用 XHR 将数据发回服务器时,它比使用 GET 要快。这是因为对少量数据而言,向服务器发送一个GET 请求要占用一个单独的数据包。另一方面,一个 POST 至少发送两个数据包,一个用于信息头。另一个用于 POST 体。 灯标此技术与动态脚本标签插入非常相似。JavaScript用于创建一个新的Image对象,将src设置为服务器上一个脚本文件的URL.此URL包含我们打算通过GET格式传回的键值对数据。注意,并没有创建img元素或者将它们插入到DOM中。123var url = '/status_tracker.php';var params = ['step=2','time=124802'];(new Image()).src=url+'?'+params.join('&'); 这是将信息返回服务器最有效的方法。可以通过监听Image对象的load事件,它可以告知服务器是否成功接收了数据。还可以检查服务器返回图片的宽度和高度(如果返回了一张图片),并用这些数字通知你服务器的状态。例如,宽度为1表示“成功”,2表示“重试”。 如果你不需要为此响应返回数据,那么你应当发送一个 204 No Content 响应代码,无消息正文。它将阻止客户端继续等待永远不会到来的消息体。 灯标是向服务器回送数据最快和最有效的方法。唯一的缺点是接收到的响应类型是受限的。如果需要向客户端发送大量数据,那么使用XHR。如果只关心将数据发送到服务器端,那么使用图像灯标。 数据格式在考虑数据格式时,唯一需要比较的尺度的就是速度。 XML当Ajax开始流行时,它选择了XML数据格式。很多事情是围绕着它做的:极端的互通性(服务器端和客户端都能够良好保持),格式严格,易于验证。 与其他格式相比,XML极其冗长。每个离散的数据片断需要大量结构,所以有效数据的比例非常低。此外,解析XML也比较繁琐。一个更有效的方式是将每个值都存储为<user>标签的属性。数据相同而文件尺寸却更小。 JSONJSON是一个轻量级并易于解析的数据格式,它按照JavaScript对象和数组字面语法所编写。在解析JSON数据的过程中,需要保持数据的顺序。 使用()可以将字符串转换为JavaScript代码。数组形式的json数据文件尺寸最小,下载最快,平均解析时间最短。 JSON-P当使用XHR时JSON数据作为一个字符串返回,该字符串使用()转换为一个本地对象。然而,当使用动态脚本标签插入时,JSON数据被视为另一个JavaScript文件并作为本地代码执行。为做到这一点,数据必须被包装在回调函数之中。这就是“JSON填充”或JSON-P。 JSON-P 因为回调包装的原因略微增加了文件尺寸,但与其解析性能的改进相比这点增加微不足道。由于数据作为本地 JavaScript 处理,它的解析速度像本地 JavaScript 一样快。 最快的JSON格式是:使用数组的JSON-P格式。 还有一个与性能无关的原因要避免使用 JSON-P:因为 JSON-P 必须是可执行的 JavaScript,它使用动态脚本标签注入技术可在任何网站中被任何人调用。从另一个角度说,JSON 在运行之前并不是有效的。JavaScript,使用 XHR 时只是被当作字符串获取。不要将任何敏感的数据编码为 JSON-P,因为你无法确定它是否包含私密信息,或者包含随机的 URL 或 cookie。 HTMLJavaScript能够比较快地将一个大数据结构转化为简单的HTML,但是服务器完成同样工作更快。一种技术考虑是在服务器端构建整个 HTML 然后传递给客户端,JavaScript 只是简单地下载它然后放入 innerHTML。此技术的问题在于,HTML是一种详细的数据格式,比XML更加冗长。因此,只有当客户端 CPU 比带宽更受限时才使用此技术。 字符串操作是JavaScript最慢的操作之一。 自定义格式最理想的数据格式只包含必要的结构,使你能够分解出每个字段。可以自定义一种格式只是简单地用一个分隔符将数据连接起来。然后调用字符串的split()方法将分隔符作为参数传入,即可分解数据。 split()是最快的字符串操作之一。123456789101112131415function parseCustomFormat(responseText) { var users = []; var usersEncoded = responseText.split(';'); var userArray; for (var i = 0, len = usersEncoded.length; i < len; i++) { userArray = usersEncoded[i].split(':'); users[i] = { id: userArray[0], username: userArray[1], realname: userArray[2], email: userArray[3] }; } return users;} 对于非常大的数据集,它是最快的传输格式,甚至可以在解析速度和下载时间上击败本机执行的JSON。用此格式向客户端传送大量数据只用很少的时间。 数据格式总结最好的格式是JSON和字符分隔的自定义格式。JSON-P数据,用动态脚本标签插入法获取。它将数据视为可运行的JavaScript代码而不是字符串,解析速度极快,并且可实现跨域。字符分隔的自定义格式,使用XHR或动态脚本标签插入技术提取,使用split()解析。 Ajax性能向导缓存数据最快的Ajax就是你不要用它。有两种主要方法避免发出一个不必要的请求:在服务器端,设置HTTP头,确保返回报文将被缓存在浏览器中;在客户端,于本地缓存已获取的数据,不要多次请求同一个数据; 设置HTTP头如果希望Ajax响应报文能够被浏览器所缓存,你必须在发起请求时使用GET方法,并且必须在响应报文中发送正确的HTTP头。Expires头告诉浏览器应当缓存响应报文多长时间。 什么是Expires头?Expires存储的是一个用来控制缓存失效的日期。当浏览器看到响应中有一个Expires头时,它会和相应的组件一起保存到其缓存中,只要组件没有过期,浏览器就会使用缓存版本而不会进行任何的HTTP请求。Expires设置的日期格式必须为GMT(格林尼治标准时间)。 Expires 头示例如下:Expires: Mon, 28 Jul 2014 23:30:00 GMT 一个 Expires 头是确保浏览器缓存 Ajax 响应报文最简单的方法。 本地存储数据除了依赖浏览器处理缓存之外,还可以使用本地存储数据的方法,将从服务器接收到的响应报文存储。将响应报文存放在一个对象中,以URL为键值索引。1234567891011121314151617181920212223var localCache ={};function xhrRequest(url, callback){ if(localCache[url]){ //检查URL此前是否被取用过 callback.success(localCache[url]); return ; } var req = createXhrObject(); req.onerror = function(){ callback.error(); }; req.onreadystatechange = function(){ if(req.readyState === 4){ if(req.responseText === '' || req.status == '404'){ callback.error(); return; } localCache[url] = req.responseText; callback.success(req.responseText); } } req.open('GET',url,true); req.send(null);} 了解Ajax库的限制所有 JavaScript 库允许你访问一个 Ajax 对象,它屏蔽浏览器之间的差异,给你一个一致的接口。大多数情况下这非常好,因为它使你可以关注你的项目,而不是那些古怪的浏览器上 XHR 的工作细节。然而,为了给你一个统一的接口,这些库必须简化接口,因为不是所有浏览器都实现了每个功能。这使得你不能访问 XMLHttpRequest的完整功能。 总结高性能Ajax包括:知道你项目的具体需求,选择正确的数据格式和与之相配的传输技术。 作为数据格式,纯文本和 HTML 是高度限制的,但它们可节省客户端的 CPU 周期。XML 被广泛应用普遍支持,但它非常冗长且解析缓慢。JSON 是轻量级的,解析迅速(作为本地代码而不是字符串),交互性与 XML 相当。字符分隔的自定义格式非常轻量,在大量数据集解析时速度最快,但需要编写额外的程序在服务器端构造格式,并在客户端解析。 进一步提高Ajax的速度的方法: 减少请求数量,可通过JavaScript和CSS文件打包,或者使用MXHR;缩短页面加载时间,在页面其他内容加载之后,使用Ajax获取少量重要文件;确保代码错误不要直接显示给用户,并在服务器端处理错误。学会何时使用一个健壮的 Ajax 库,何时编写自己的底层 Ajax 代码。 第八章 编程实践避免二次解析JavaScript与其他的许多脚本语言一样,允许你在程序中获取一个包含代码的字符串然后运行它。有四种方法可以实现:eval(),Function()构造器,setTimeout()和setInterval()。每个函数运行传入一串JavaScript代码,然后运行它。 当在JavaScript代码中执行(另一段)JavaScript代码时,需要付出二次解析的代价。此代码先被解析为正常代码,然后在运行过程中,运行字符串中的代码时发生另一次解析。二次解析是非常昂贵的操作,与直接包含相应代码相比将占用更多的时间。 每次调用eval()/Function()构造器/setTimeout()/setInterval()时要创建一个新的解释/编译实例,使得代码执行速度变慢。 因此,应尽量避免使用eval()和Function()构造器,在使用setTimeout()和 setInterval()时,建议第一个参数传入一个函数而不是一个字符串。 使用对象/数组直接量在JavaScript中有多种方法创建对象和数组,但没有什么比创建对象和数组直接量速度更快了。直接量赋值很快,并且在代码中占用较少空间,所以整个文件尺寸可以更小。12345678910//对象直接量var myObject = { name : "Peter", count : 50, flag : true, pointer : null};//数组直接量var myArray = ["Peter",50,true,null]; 不要重复工作在计算机科学领域最重要的性能优化技术之一是避免工作。避免工作的概念实际上意味着两件事:不要做不必要的工作;不要做重复做已经完成的工作。 最常见的重复工作类型是浏览器检测。有多种办法可以避免重复工作:延迟加载(lazy loading)延迟加载意味着在信息被使用之前不做任何工作。12345678910111213141516171819202122232425262728function addHandler(target, eventType, handler){ if(target.addEventListener){ //重写addHandler()函数 addHandler = function(target,eventType,handler){ target.addEventListener(eventType,handler,false); }; }else{ //重写addHandler()函数 addHandler = function(target,eventType,handler){ target.attachEvent("on"+eventType,handler); }; } //调用函数 addHandler(target,eventType,handler);}function removeHandler(target, eventType, handler){ if (target.removeEventListener){ removeHandler = function(target, eventType, handler){ target.addEventListener(eventType, handler, false); }; } else { removeHandler = function(target, eventType, handler){ target.detachEvent("on" + eventType, handler); }; } removeHandler(target, eventType, handler);} 上述两个方法第一次被调用时,检查一次并决定使用哪种方法附加或分离事件句柄。然后原始函数就被包含适当操作的新函数覆盖了。 延迟加载适用于函数不会在页面上立即被用到的场合。 条件预加载条件预加载,在脚本加载之前提前检查,而不等待函数调用。1234567891011121314151617//使用三元操作符进行判断var addHandler = document.body.addEventListener ?function(target,eventType,handler){ target.addEventListener(eventType,handler,false);} : function(target,eventType,handler){ target.attachEvent("on"+eventType,handler);};//使用三元操作符进行判断var removeHandler = document.body.removeEventListener ?function(target, eventType, handler){ target.removeEventListener(eventType, handler, false);} : function(target, eventType, handler){ target.detachEvent("on" + eventType, handler);}; 条件预加载确保所有函数调用时间相同。适用于一个函数马上就会被用到,而且在整个页面生命周期中经常使用的场合。 使用速度快的部分虽然 JavaScript 速度慢很容易被归咎于引擎,然而引擎通常是处理过程中最快的部分,实际上速度慢的是你的代码。 位操作运算符JavaScript中有四种位逻辑操作符位与:两个操作数的位都是1,结果才是1;位或:有一个操作数的位是1,结果就是1;位异或:两个操作数的位中只有一个1,结果才是1;位非:遇0返回1,反之亦然。 有许多方法可以使用位运算提高JavaScript的速度。首先可以用位运算替代纯数学操作。例如,通常采用对2取模运算实现表行颜色交替显示。在32 位数字的底层(二进制)表示法,偶数的最低位是 0, 奇数的最低位是 1。 如果此数为偶数, 那么它和 1 进行位与操作的结果就是 0;如果此数为奇数,那么它和 1 进行位与操作的结果就是 1。1234567for(var i = 0,len = rows.length; i < len; i++){ if(i&1){ className="odd"; }else{ className="even"; }} 第二种使用位操作的技术称作位掩码。位掩码可以同时判断多个布尔选项,快速地将数字转换为布尔标志数组。掩码中每个选项的值等于2的幂。 原生方法JavaScript 的原生部分——在你写代码之前它们已经存在于浏览器之中了——都是用低级语言写的,速度非常快。例如JavaScript中的Math对象,处理复杂的数学计算应该优先考虑。 当原生方法可用时,尽量使用它们,尤其是数学运算和DOM操作。 总结通过避免使用 eval()和 Function()构造器避免二次解析。此外,给 setTimeout()和 setInterval()传递函数参数而不是字符串参数;创建新对象和数组时使用对象直接量和数组直接量;避免重复进行相同工作。当需要检测浏览器时,使用延迟加载或条件预加载;当执行数学运算时,考虑使用位操作,它直接在数字底层进行操作;原生方法总是比 JavaScript 写的东西要快。尽量使用原生方法。 第九章 创建并部署高性能JavaScript应用程序Apache AntApache Ant 是一个自动构建软件的工具。类似于make,在Java中实现,并使用XML来描述生成。Ant具有可移植性。默认的 Ant 开发文件为 XMl 格式的 build.xml。每个开发文件只包含一个项目和至少一个目标体。一个Ant 目标体可依赖于其他目标体。 合并JavaScript文件第一个也是最重要的提高速度的准则是:减少渲染页面所需的HTTP请求。合并资源文件能够减少HTTP请求。Apache Ant 通过 concat 任务提供合并几个文件的能力 预处理JavaScript文件预处理器的任务是将输入数据处理成另一种编程语言所使用的数据。没有专门为 JavaScript 设计的预处理器。 JavaScript紧凑JavaScript 紧凑指的是剔除一个 JavaScript 文件中一切运行无关内容的过程。包括注释和不必要的空格。常用YUI压缩器对JavaScript进行压缩。 除了剔除注释和不必要的空格,YUI 压缩器还提供以下功能:将局部变量名替换以更短的形式(1 个,2 个,或 3 个字符),以优化后续的 gzip 压缩工作;尽可能将中括号操作符替换为点操作符(例如 foo:[“bar”]变成 foo.bar);尽可能替换引号直接量属性名(例如{“foo”:”bar”}变成{foo:”bar”});替换字符串中的转义引号(例如’aaa\’bbb’变成”aaa’bbb”);常量折叠(例如”foo”+”bar”变成”foobar”)。 通过以下步骤,还可以进一步优化,节省空间: 将局部引用存储在对象/值中,用闭包封装代码,使用常量替代重复值,避免 eval(以及相似的 Function 构造器,setTimeout 和 setInterval 使用字符串直接量作为第一个参数),with 关键字,JScript 条件注释,都有助于进一步精缩文件。 开发过程中的编译时和运行时连接,预处理,和紧凑既可以在编译时发生,也可以在运行时发生。 开发高性能应用程序的一个普遍规则是,只要能够在编译时完成的工作,就不要在运行时去做。 JavaScript 压缩当网页浏览器请求一个资源时,它通常发送一个Accept-Encoding的HTTP头(以HTTP/1.1开始)让网页服务器知道传输所支持的编码类型。Accept-Encoding 的取值范围是:gzip,compress,deflate和 identity。gzip通常可将有效载荷减少到70%,主要用于文本报文,包括JavaScript文件。 缓存JavaScript文件使用HTTP组件可缓存将大大提高用户再次访问网站时的用户体验。网页服务器使用 Expires 响应报文 HTTP 头让客户端知道缓存资源的时间。另一种技术是使用HTML5离线应用程序缓存。此技术依赖一个配置文件,列出应当被缓存的资源。此配置文件通过<html>标签的manifest属性。12<!DOCTYPE html><html manifest="demo.manifest"> 关于缓存的问题充分利用缓存控制可真正提高用户体验,但它有一个缺点:当应用程序更新之后,你希望确保用户得到静态内容的最新版本。这通过对改动的静态资源进行重命名实现。 使用内容分发网络(cdn)内容分发网络(CDN)是按照地理位置分布的计算机网络,通过以太网负责向最终用户分发内容。使用CDN的主要原因是可靠性,可扩展性,但更主要的是性能。事实上,通过地理位置上最近的位置向用户提供服务,CDN可以极大地减少网络延迟。 总结开发和部署过程对基于 JavaScript 的应用程序可以产生巨大影响,最重要的几个步骤如下:合并 JavaScript 文件,减少 HTTP 请求的数量;使用 YUI 压缩器紧凑处理 JavaScript 文件;以压缩形式提供 JavaScript 文件(gzip 编码);通过设置 HTTP 响应报文头使 JavaScript 文件可缓存,通过向文件名附加时间戳解决缓存问题;使用内容传递网络(CDN)提供 JavaScript 文件,CDN 不仅可以提高性能,它还可以为你管理压缩和缓存。 第十章 工具当确定脚本加载和运行时的瓶颈所在时,合手的工具是必不可少的。 性能分析在脚本运行期定时执行不同函数和操作,找出需要优化的部分。性能分析工具确保优化工作花费在系统中最慢,影响大多数浏览器的地方,而不是去判定那些函数或操作缓慢。 网络分析检查图片,样式表和脚本的加载过程,汇报它们对整个页面加载和渲染的影响 YUI分析器YUI 分析器是用 JavaScript 编写的JavaScript 分析器。除了计时功能,它还提供了用于函数、对象、和构造器的性能分析接口,还包括性能分析数据的详细报告。它可以跨浏览器工作,其输出数据可提供更强大的报告和分析。1Y.Profiler.registerFunction("initUI"); FirebugFirebug 提供了一个控制台日志输出, 当前页面的 DOM 树显示, 样式信息, 能够反观 DOM 和 JavaScript对象,以及更多功能。它还包括一个性能和网络分析器,这是本节的重点。Firebug 易于扩展,可添加自定义面板。 脚本阻塞浏览器每次只能发出一个脚本请求。这样做是为了管理文件之间的以来关系。只要一个文件依赖于另一个在源码中靠后的文件,它所依赖的文件将保证在它运行之前被准备好。脚本之间的差距表明脚本被阻塞了。脚本阻塞将因为一个或多个文件初始化缓慢而变得更加严重,值得对它做某些类型的分析,并有可能优化或重构。脚本加载会减慢或停止页面渲染,造成用户等待。 总结使用网络分析器找出加载脚本和其它页面资源的瓶颈所在,这有助于决定哪些脚本需要延迟加载,或者进行进一步分析;应尽量减少 HTTP 请求的数量,尽量延迟加载脚本以使页面渲染速度更快,向用户提供更好的整体体验;使用性能分析器找出脚本运行时速度慢的部分,检查每个函数所花费的时间,以及函数被调用的次数,通过调用栈自身提供的一些线索来找出哪些地方应当努力优化。]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>高性能JavaScript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[《高性能JavaScript》读书笔记(二)]]></title>
<url>%2F2017%2F05%2F21%2F%E7%AC%94%E8%AE%B0%2F%E3%80%8A%E9%AB%98%E6%80%A7%E8%83%BDJavaScript%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89%2F</url>
<content type="text"><![CDATA[第四章 算法和流程控制代码的整体结构是执行速度的决定因素之一。性能损失与代码组织方式和具体问题解决办法直接相关。 循环循环是最常用的模式之一,理解JavaScript中循环对性能的影响至关重要,因为死循环或者长时间的循环会严重影响用户体验。 循环的类型ECMA标准定义了四种类型的循环。第一个是标准的for循环。for循环是最常用的JavaScript循环结构,它由四部分组成:初始化体,前测条件,后执行体,循环体。for循环封装上的直接性是开发者喜欢的原因。 第二种循环是while循环。while循环是一个简单的预测试循环,由一个预测条件和一个循环体构成。任何for循环都可以写成while循环。 第三种循环类型是do-while循环。do-while循环是JavaScript中唯一一种后测试的循环,它包括两部分:循环体和后测试条件体。do-while循环中,循环体至少运行一次。 第四种循环称为for-in循环。此循环有一个非常特殊的用途:它可以枚举任何对象的命名属性,基本格式如下:123for(var prop in object){ //loop body} 每次循环执行,属性变量被填充以对象属性的名字(一个字符串),直到所有的对象属性遍历完成才返回。返回的属性包括对象的实例属性和它从原型链继承而来的属性。 循环性能在四种循环中,只有for-in循环比其他循环明显要慢。因此推荐的做法是:除非你需要对数目不详的对象属性进行操作,否则避免使用for-in循环。如果迭代遍历一个有限的,已知的属性列表,使用如下模式:12345var props = ["prop1","prop2"],i = 0;while(i < props.length){ process(object[props[i]]); i++;} 其他三种循环性能相当,选择循环应基于需求而不是性能。考虑如下两个因素: 每次迭代干什么?迭代的次数。通过减少这两者中一个或全部(的执行时间),你可以积极地影响循环的整体性能。 减少迭代的工作量限制在循环体内进行耗时操作的数量是一个加快循环的好方法。 在一个典型的数组处理循环中,每次运行循环体都要发生如下几个操作:1.在控制条件中读一次属性(items.length);2.在控制条件中执行一次比较(i < items.length);3.比较操作,察看条件控制体的运算结果是否为true(i < items.length == true);4.一次自加操作(i++);5.一次数组查找(items[i]);6.一次函数调用(process(items[i]))。 在简单的循环中,每次迭代也要进行许多操作。减少每次迭代中操作的总数可以大幅度提高循环整体性能。 优化循环工作量的第一步是减少对象成员和数组项查找的次数。例如,将数组的长度(items.length)赋值给一个局部变量,在控制条件中使用这个局部变量,从而提高循环的性能。还可以通过改变数组元素的顺序提高循环性能。倒序循环是编程语言中常用的性能优化方法。 采用倒序循环后,每次迭代只进行如下操作:1.在控制条件中进行一次比较(i == true);2.一次减法操作(i–);3.一次数组查询(items[i]);4.一次函数调用(process(items[i])); 倒序循环的每次迭代中减少两个操作,随着迭代次数的增长,性能将显著提升。 减少迭代次数即使循环体中最快的代码,累计迭代上千次(也将是不小的负担)。所以,减少循环的迭代次数可获得显著的性能提升。最广为人知的限制循环迭代次数的模式称作“达夫设备”。 达夫设备是一个循环体展开技术,在一次迭代中实际上执行了多次迭代操作。典型实现如下:12345678910111213141516var iterations = Math.floor(items.length/8), startAt = items.length%8,i = 0;do{ switch(startAt){ case 0:process(items[i++]); case 7:process(items[i++]); case 6:process(items[i++]); case 5:process(items[i++]); case 4:process(items[i++]); case 3:process(items[i++]); case 2:process(items[i++]); case 1:process(items[i++]); } startAt = 0;}while(--iterations); 达夫设备背后的基本理念是:每次循环中最多可8次调用process()函数。循环迭代次数为元素总数除以8.因为总数不一定是8的整数倍,所以startAt变量存放余数,指出第一次循环中应当执行多少次process(); 此算法一个稍快的版本取消了switch表达式,将余数处理和主循环分开:123456789101112131415var i = items.length%8;while(i){ process(items[i--]);}i = Math.floor(items.length/8);while(i){ process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]);} 是否值得使用达夫设备,取决于迭代的次数,如果循环迭代次数少于1000次,它提升的性能微不足道,但迭代次数超过1000次,达夫设备的效率将明显提升。 基于函数的迭代ECMA-262标准介绍了本地数组对象的一个新方法forEach()。此方法遍历一个数组的所有成员,并在每个成员上执行一个函数。在每个元素上执行的函数作为forEach()的参数传进去,并在调用时接收三个参数,它们是:数组项的值,数组项的索引和数组自身。123items.forEach(function(value,index,array){ process(value);}); forEach()函数在Firefox,Chrome和Safari中为原生函数。另外在JavaScript库都有等价实现:1234567891011121314151617181920//YUI 3Y.Array.each(items, function(value, index, array){ process(value);});//jQueryjQuery.each(items, function(index, value){ process(value);});//Dojodojo.forEach(items, function(value, index, array){ process(value);});//Prototypeitems.each(function(value, index){ process(value);});//MooTools$each(items, function(value, index){ process(value);}); 基于函数的迭代更加便利,但还是比基于循环的迭代要慢一些。每一个数组项要关联额外的函数调用是造成速度慢的原因。在所有情况下,基于函数的迭代占用时间是基于循环的迭代的八倍,因此在关注执行时间的情况下它并不是一个合适的方法。 条件表达式与循环相似,条件表达式决定JavaScript运行流的走向。 if-else与switch比较使用if-else或者switch的流行理论是基于测试条件的数量:条件数量较大,倾向于使用switch而不是if-else。在大多数情况下switch表达式比if-else更快,但只有当条件数量很大时才明显更快。两者间的主要性能区别在于:当条件体增加时,if-else性能负担增加的程度比switch更多。一般来说,if-else适用于判断两个离散的值或者判断几个不同的值域。如果判断多于两个离散值,switch表达式会更好。 优化if-else优化if-else的目标总是最小化找到正确分支之前所判断条件体的数量。最简单的优化方法是将最常见的条件体放在首位。if-else 中的条件体应当总是按照从最大概率到最小概率的顺序排列,以保证理论运行速度最快。 另一种减少条件判断数量的方法是将if-else组织成一系列嵌套的if-else表达式。使用一个单独的一长串的if-else通常导致运行缓慢,因为每个条件体都要被计算。 查表法有些情况下要避免使用if-else或switch。当有大量离散值需要测试时,if-else和switch都比使用查表法要慢得多。在JavaScript中,查表法可使用数组或普通对象实现。12var results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10]return results[value]; 使用查表法时,必须完全消除所有条件判断。操作转换成一个数组项查询或者一个对象成员查询。使用查表法的一个优点是:由于没有条件判断,当候选值数量增加时,很少,甚至没有增加额外的性能开销。 递归复杂算法通常比较容易使用递归实现。但递归也存在些问题:一个错误定义,或者缺少终结条件可导致长时间运行,冻结用户界面。此外,递归函数还会遇到浏览器调用栈大小的限制。 调用栈限制JavaScript引擎所支持的递归数量与JavaScript调用栈大小直接相关。只有IE例外,它的调用栈与可用系统内存相关,其他浏览器有固定的调用栈限制。Chrome 是唯一不显示调用栈溢出错误的浏览器。关于调用栈溢出错误,最令人感兴趣的部分大概是:在某些浏览器中,他们的确是 JavaScript 错误,可以用一个 try-catch 表达式捕获。异常类型因浏览器而不同。在 Firefox 中,它是一个 InternalError;在 Safari和 Chrome 中,它是一个 RangeError;在 Internet Explorer 中抛出一个一般性的 Error 类型。 (Opera 不抛出错误;它终止 JavaScript 引擎)。这使得我们能够在 JavaScript 中正确处理这些错误:12345try{ recurse();}catch(ex){ alert("Too much recursion!");} 递归模式1.直接递归模式(函数调用自身)1234function recurse(){ arguments.callee();}recurse(); 2.精巧模式包含两个函数:1234567function first(){ second();}function second(){ first();}first(); 常见的栈溢出原因是一个不正确的终止条件,所以定位模式错误的第一步是验证终止条件。如果终止条件正确,那么算法包含了太多层递归,为了能够安全地在浏览器中运行,应该改用迭代,制表,或两者兼而有之。 迭代任何可以用递归实现的算法都可以用迭代实现。使用优化的循环替代长时间运行的递归函数可以提高性能,因为运行一个循环比反复调用一个函数的开销要低。合并排序算法是最常用的以递归实现的算法。123456789101112131415161718192021function merge(left, right){ var result = []; while(left.length > 0 && right.length > 0){ if(left[0] < right[0]){ result.push(left.shift()); }else{ result.push(right.shift()); } } return result.concat(left).concat(right);}function mergeSort(items){ if(items.length == 1){ return items; } var middle = Math.floor(items.length/2), left = items.slice(0,middle), right = items.slice(middle); return merge(mergeSort(left), mergeSort(right));} 上述合并排序代码相当简单,但是 mergeSort()函数被调用非常频繁,可能在Firefox上导致栈溢出。改用迭代实现合并排序算法:1234567891011121314151617181920212223242526272829function merge(left, right){ var result = []; while(left.length > 0&&right.length > 0){ if(left[0] < right[0]){ result.push(left.shift()); }else{ result.push(right.shift()); } } return result.concat(left).concat(right);}function mergeSort(items){ if(items.length == 1){ return items; } var work = []; for(var i = 0,len = items.length; i < len; i++){ work.push([items[i]]); } work.push([]); for(var lim = len; lim > 1; lim = (lim + 1) / 2){ for(var j = 0, k = 0; k < lim; j++, k += 2){ work[j] = merge(work[k], work[k + 1]); } work[j] = []; } return work[0];} 此mergeSort()函数采用迭代而不是递归,虽然迭代版本的合并排序可能比递归版本慢些,但它不会像递归版本那样影响调用栈。将递归算法切换为迭代是避免栈溢出错误的方法之一。 制表减少工作量就是最好的性能优化技术。制表,通过缓存先前计算结果为后续计算所重复使用,避免了重复工作。因此,常常结合制表来实现递归算法。123456789101112function memfactorial(n){ if(!memfactorial.cache){ memfactorial.cache = { "0": 1, "1": 1 }; } if (!memfactorial.cache.hasOwnProperty(n)) { memfactorial.cache[n] = n * memfactorial(n-1); return memfactorial.cache[n];} 上述代码使用了制表技术的阶乘函数的关键是建立一个缓存对象。此对象位于函数内部,并预置了两个最简单的阶乘:0和1。在计算阶乘之前,首先检查缓存中是否已经存在相应的计算结果。没有对应的缓冲值说明这是第一次进行此数值的计算,计算完成之后结果被存入缓存之中,以备今后使用。 为了使一个函数的制表过程更加容易,可定义一个memoize()函数封装基本功能:12345678910function memoize(fundamental,cache){ cache = cache || {}; var shell = function(arg){ if(!cache.hasOwnProperty(arg)){ cache[arg] = fundamental(arg); } return cache[arg]; }; return shell;} 此memoize()函数接收两个参数:一个用来制表的函数和一个可选的缓存对象。调用如下:1var memfactorial = memoize(factoroal, {"0": 1,"1":1}); 总结与其它语言不同的是,JavaScript可用资源有限。1、for,while,do-while循环的性能特性相似,谁也不比谁更快或更慢;2、除非你要迭代遍历一个属性未知的对象,否则不要使用for-in循环;3、改善循环性能的最好办法是减少每次迭代中的运算量,并减少循环迭代次数;4、一般来说,switch总是比if-else更快,但并不总是最好的解决方法;5、当判断条件较多时,查表法比if-else或者switch更快;6、浏览器的调用栈尺寸限制了递归算法在JavaScript中的应用:栈溢出错误导致其他代码也不能正常执行;7、如果遇到一个栈溢出错误,将方法修改为一个迭代算法或者使用制表法可以避免重复工作; 第五章 字符串和正则表达式字符串拼接字符串拼接表现出惊人的性能紧张。 字符串拼接的方法: “+”和“+=”操作符;数组的join()方法;字符串的concat()方法。1str += "one"+"two"; 此代码执行时,发生四个步骤: 内存中创建了一个临时字符串 临时字符串的值被赋予“onetwo” 临时字符串与str的值进行连接 结果赋予str 下面的代码通过两个离散表达式直接将内容附加在str上,避免了临时字符串。在大多数浏览器上这样做可加快10%-40%。12str += "one";str += "two"; 或者1str = str + "one" +"two"; 使用这种方法进行拼接字符串,必须把str放在第一个拼接的字符串。 除IE以外,浏览器尝试扩展表达式左端字符串的内存,然后简单地将第二个字符串拷贝到它的尾部。如果一个循环中,基本字符串位于最左端,就可以避免多次复制一个越来越大的基本字符串。 在 IE8 中,连接字符串只是记录下构成新字符串的各部分字符串的引用。在最后时刻(当你真正使用连接后的字符串时),各部分字符串才被逐个拷贝到一个新的“真正的”字符串中,然后用它取代先前的字符串引用,所以并非每次使用字符串时都发生合并操作。 IE7 和更早的浏览器在连接字符串时使用更糟糕的实现方法,每连接一对字符串都要把它们复制到一块新分配的内存中。 Firefox和编译期合并在赋值表达式中所有字符串连接都属于编译期常量,Firefox自动地在编译过程中合并它们。123456789101112131415function foldingDemo(){ var str = "compile" + "time" + "folding"; str += "this" + "works" + "too"; str = str + "but" + "not" + "this";}alert(foldingDemo.toString());// 输出:/*function foldingDemo(){ var str = "compile" + "time" + "folding"; str += "this" + "works" + "too"; str = str + "but" + "not" + "this";}*/ 当字符串是这样合并在一起时,由于运行时没有中间字符串,所有连接它们的时间和内存可以减少到零。这种功能非常了不起,但它并不经常起作用,因为通常从运行期数据创建字符串而不是编译期常量。 数组联结(在IE7以及更早的浏览器中,效果显著)Array.prototype.join方法将数组的所有元素合并成一个字符串,并在每个元素之间插入一个分隔符字符串。在大多数浏览器上,数组联结比连接字符串的其他方法更慢,但事实上,为一种补偿方法,在IE7和更早的浏览器上,它是连接大量字符串唯一高效的途径。 String.prototype.concat原生字符串连接函数接受任意数目的参数,并将每一个参数都追加在调用函数的字符串上。可以追加任意个字符串,或者一个完整的字符串数组。但在大多数情况下concat比简单的”+”和”+=”慢一些,而且在IE,Opera和Chrome上大幅变慢。 正则表达式优化粗浅地编写正则表达式是造成性能瓶颈的主要原因。 第六章 响应接口确保网页应用程序的响应速度也是一个重要的性能关注点。 大多数浏览器有一个单独的处理进程,它由两个任务所共享:JavaScript任务和用户界面更新任务。每个时刻只有其中一个操作得以执行,即当JavaScript运行时,用户界面就被”锁定“了。 浏览器UI线程JavaScript和UI更新共享的进程通常被称作浏览器UI线程。此UI线程围绕一个简单的队列系统工作,任务被保存到队列中直至进程空闲。一旦空闲,队列中的下一个任务将被检索和运行。此进程中最令人感兴趣的部分是每次输入均导致一个或多个任务被加入队列。按下一个按钮,然后屏幕上显示出一个消息:123456789101112131415<html><head><title>Browser UI Thread Example</title></head><body><button onclick="handleClick()">Click Me</button><script type="text/javascript">function handleClick(){ var div = document.createElement("div"); div.innerHTML = "Clicked!"; document.body.appendChild(div);}</script></body></html> 线程触发的过程 浏览器限制浏览器在JavaScript运行时间上采取了限制。这是一个有必要的限制,确保恶意代码编写者不能通过无尽的密集操作锁定用户浏览器或计算机。此类限制有两个:调用栈尺寸限制和长时间脚本限制。 长运行脚本限制有时被称作长运行脚本定时器或者失控脚本定时器,但其基本思想是浏览器记录一个脚本的运行时间,一旦到达一定限度时就终止它。 有两种测量脚本运行时间的方法:1)统计自脚本开始运行以来执行过多少语句;2)统计脚本运行的总时间。 长运行脚本最好的处理办法首先是避免他们。 多久才算”太久“一个单一的JavaScript操作应当使用的总时间(最大)是100毫秒。如果某个接口在100毫秒内响应用户输入,用户认为自己是”直接操作用户界面的对象“,超过100毫秒意味着用户认为自己与接口断开了。 用定时器让出时间片在某些情况下,还是有一些JavaScript任务因为复杂性原因不能再100毫秒或更少时间内完成。这种情况下,理想办法是让出对UI线程的控制,使UI更新可以进行。 定时器基础在JavaScript中使用setTimeout()或setInterval()创建定时器。setTimeout()函数创建一个定时器只运行一次,而setInterval()函数创建一个周期性重复运行的定时器。 定时器与UI线程交互的方式有助于分解长运行脚本成为较短的片断。调用setTimeout()或setInterval()告诉JavaScript引擎等待一定时间,然后将JavaScript任务添加到UI队列中。12345function greeting(){ alert("Hello world!");}setTimeout(greeting, 250); 第二个参数”250“指出的是什么时候应当将任务添加到UI队列,并不是说那时代码就将被执行,这个任务必须等到队列中的其他任务都执行之后才能被执行。 定时器代码只有等创建它的函数运行完成之后,才有可能被执行。在任何一种情况下,创建一个定时器造成UI线程暂停,如同它从一个任务切换到下一个任务。因此,定时器代码复位所有相关的浏览器限制,包括长运行脚本时间。此外调用栈也在定时器代码中复位为零。 如果调用 setTimeout()的函数又调用了其他任务,耗时超过定时器延时,定时器代码将立即被执行,它与主调函数之间没有可察觉的延迟。最小定时器延时应设置25毫秒。 在数组处理中使用定时器一个常见的长运行脚本就是循环占用了太长的运行时间。经过循环优化后,还是不能缩减足够的运行时间,那么定时器还能更进一步地优化。基本方法是:将循环工作分解到定时器序列中。123for(var i = 0,len = items.length; i < len; i++){ process(items[i]);} 上述循环结构运行时间过长的原因有二:process()的复杂度,items的大小。 判断是否可用定时器取代循环的两个决定性因素?1)此处理过程必须是同步处理吗?2)数据必须按顺序处理吗? 如果两个回答都是“否”,那么代码将适于使用定时器分解工作。一种基本异步代码模式如下:12345678910111213141516171819function processArray(items, process, callback){ var todo = items.concat(); setTimeout(function(){ process(todo.shift()); if(todo.length > 0){ setTimeout(arguments.callee,25); }else{ callback(items); } }, 25);}var items = [123, 789, 323, 778, 232, 654, 219, 543, 321, 160];function outputValue(value){ console.log(value);}processArray(items, outputValue, function(){ console.log("Done!");}); 上述代码的基本思想是: 创建一个原始数组的克隆,将它作为处理对象。第一次调用setTimeout()创建一个定时器处理队列中的第一个项。调用todo.shift()返回它的第一个项然后将它从数组中删除。此值作为参数传给process()。然后,检查是否还有更多项需要处理。如果todo队列中还有内容,那么再启动一个定时器。如果不再有内容需要处理,将调用callback()函数。 分解任务我们通常将一个任务分解成一系列子任务。可将一行代码简单地看作一个原子任务,多行代码组合在一起构成一个独立任务。如果函数运行时间太长,它可以拆分成一系列更小的步骤,把独立方法放在定时器中调用。1234567891011121314151617function saveDocument(id){ openDocument(id) writeText(id); closeDocument(id); updateUI(id);}function saveDocument(id){ var tasks = [openDocument, writeText, closeDocument, updateUI]; setTimeout(function(){ var task = tasks.shift(); task(id); if (tasks.length > 0){ setTimeout(arguments.callee, 25); } }, 25);} 不要让任何JavaScript代码持续运行超过50毫秒,确保代码永远不会影响用户体验。通过原生的Date对象跟踪代码的运行时间。这是大多数JavaScript分析工具所采用的工作方式:123456789var start = +new Date(), //加号“+”将Date对象转换为一个数字 stop;someLongProcess();stop = +new Date();if(stop - start <50){ alert("something");}else{ alert("taking too long.");} 有时每次只执行一个任务效率不高。在定时器循环中加入时间检测机制,使得每次处理多个数据,这样避免了将任务分解成过于碎小的片断。 定时器和性能定时器使得JavaScript代码整体性能表现出巨大差异,但过度使用它们会对性能产生负面影响。使用定时器序列时,同一时间只有一个定时器存在,只有当这个定时器结束才创建一个新的定时器。此外,还应该在web应用中限制高频率重复定时器的数量。建议创建一个单独的重复定时器,每次执行多个操作。 Web Workers(网页工人线程)自JavaScript诞生以来,还没有办法在浏览器UI线程之外运行代码。Web workers API 改变了这种状况,它引入一个接口,使代码运行而不占用浏览器UI线程的时间。web workers在自己的线程中运行JavaScript,意味着,web workers中的代码运行不仅不会影响浏览器UI,而且也不会影响其他web workers中运行的代码。 工人线程运行环境由于web workers不绑定UI线程,这也意味着它们将不能访问更多的浏览器资源。Web workers 修改DOM将导致用户界面出错,但每个web worker都有自己的全局运行环境,只有JavaScript特性的一个子集可用。 工人线程的运行环境由下列部分组成: 一个浏览器对象,只包含四个属性:appName,appVersion,userAgent和platform;一个location对象(和window对象一样,只是所有属性都是只读的);一个self对象指向全局工人线程对象;一个importScript()方法,使得工人线程可以加载外部JavaScript文件;所有ECMAScript对象,诸如Object,Array,Date等等;XMLHTTPRequest构造器;setTimeout()和setInterval()方法;一个close()方法,可以立即停止工人线程。 因为web worker有不同的全局运行环境,你不能在JavaScript代码中创建。需要创建一个完全独立的JavaScript文件,包含那些在工人线程中运行的代码。要创建web workers,必须要传入这个JavaScript文件的URL:1var worker = new Worker("code.js"); 此代码一旦执行,将为指定文件创建一个新线程和一个新的工人线程运行环境。此文件被异步下载,直到下载并运行完之后才启动工人线程。 工人线程交互工人线程和网页代码通过事件接口进行交互。网页代码可通过postMessage()方法向工人线程传递数据,它接收单个参数,即传递给工人线程的数据。此外,在工人线程中还有onmessage事件句柄用于接收信息。123456789101112131415161718//WEB页主线程var worker = new Worker("worker.js"); //创建一个Worker对象并向它传递将在新线程中执行的脚本的URLworker.postMessage("hello world"); //向worker发送数据worker.onmessage =function(evt){ //接收worker传过来的数据函数 console.log(evt.data); //输出worker发送来的数据}//worker.jsonmessage =function (evt){ var d = evt.data; //通过evt.data获得发送来的数据 postMessage( d ); //将获取到的数据发送会主线程} 消息系统是页面和工人线程之间唯一的交互途径。只有某些类型的数据可以使用postMessage()传递。可以传递的类型有:string,number,boolean,null,undefined以及Objec和Array的实例。 加载外部文件当工人线程用过importScripts()方法加载外部JavaScript文件,它接收一个或多个URL参数,指出要加载的JavaScript文件网址。工人线程以阻塞方式调用importScripts(),直到所有文件加载完成并执行之后,脚本才继续运行。但由于工人线程在UI线程之外运行,这种阻塞不会影响UI响应。 Web Workers的实际用途Web Workers适合于那些纯数据的,或者与浏览器UI没关系的长运行脚本。例如:解析一个很大的 JSON 字符串(JSON 解析将在后面第七章讨论)。假设数据足够大,至少需要 500 毫秒才能完成解析任务。很显然时间太长了以至于不能允许 JavaScript 在客户端上运行它,因为它会干扰用户体验。此任务难以分解成用于定时器的小段任务,所以工人线程成为理想的解决方案。12345678910111213var worker = new Worker("jsonparser.js");worker.onmessage = function(event){ var jsonData = event.data; evaluateData(jsonData);};worker.postMessage(jsonText);//jsonparser.jsself.onmessage = function(event){ var jsonText = event.data; var jsonData = JSON.parse(jsonText); self.postMessage(jsonData);}; 此工程只能在Firefox 3.5和更高版本中运行,而Safari 4 和Chrome 3中,页面和工人线程之间只允许传递字符串。 解析一个大字符串只是许多受益于web workers的任务之一。其他可能受益的任务如下: 编/解码一个大字符串;复杂数学运算(包括图像或视频处理);给一个大数组排序。 任何超过100毫秒的处理,都应当考虑工人线程方案是不是比基于定时器的方案更合适。当然,还要基于浏览器是否支持工人线程。 总结JavaScript和用户界面更新在同一个进程内运行,同一时刻只有其中一个可以运行。有效管理UI线程就是要确保JavaScript不能运行太长时间,以免影响用户体验。牢记以下几点: JavaScript运行时间不应该超过100毫秒;JavaScript运行期间,浏览器响应用户交互的行为存在差异。无论如何,JavaScript长时间运行将导致用户体验混乱和脱节;定时器可用于安排代码推迟执行,它使得你可以将长运行脚本分解成一系列较小的任务;Web Workers是新式浏览器才支持的特性,它允许你在UI线程之外运行JavaScript代码而避免锁定UI。]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>高性能JavaScript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[《高性能JavaScript》读书笔记(一)]]></title>
<url>%2F2017%2F05%2F20%2F%E7%AC%94%E8%AE%B0%2F%E3%80%8A%E9%AB%98%E6%80%A7%E8%83%BDJavaScript%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89%2F</url>
<content type="text"><![CDATA[第一章 加载和运行JavaScript拥有阻塞的特征,当JavaScript运行时,其他的事情不能被浏览器处理。大多数浏览器使用单进程处理UI更新和JavaScript运行等多个任务,而同一时间只能有一个任务被执行。 <script>标签一旦出现,就会使整个页面因脚本解析、运行而出现等待。因为脚本可能在运行过程中修改页面内容。比如使用document.write()方法。 浏览器在遇到<body>标签之前,不会渲染页面的任何部分。将脚本放在页面的顶端,将导致一个可以察觉的延迟,通常表现为:页面打开时,首先显示为一副空白的页面,而此时用户不能进行阅读和操作。 IE8,Firefox 3.5,Safari 4和Chrome 2允许并行下载JavaScript文件,但JavaScript的下载仍然要阻塞其他资源的下载,例如图片等。 因此,推荐将所有的<script>标签放在尽可能接近<body>标签底部的位置,尽量减少对整个页面下载的影响。 此外,限制页面的<script>标签的数量也可以改善性能。每当页面解析碰到一个<script>标签时,紧接着有一段时间用于代码执行。最小化这些延迟时间可以改善页面的整体性能。每个HTTP请求都会产生额外的性能负担,下载一个100KB的文件比下载四个25KB的文件要快,所以减少引用外部脚本文件的数量,就可以减少性能损失。 可以使用打包工具或者使用CDN(内容分发网络)来实现:例如,下面的URL包含两个文件(YUI,联合句柄) http://yui.yahooapis.com/combo?2.7.0/build/yahoo/yahoo-min.js&2.7.0/build/event/event-min.js此URL请求两个文件,这两个文件在服务器是分离的,但当服务器收到此URL请求时,两个文件会被合并在一起返回给客户端,从而减少了<script>标签 注意: 保持 JavaScript 文件短小,并限制 HTTP 请求的数量,只是创建反应迅速的网页应用的第一步。一个应用程序所包含的功能越多,所需要的 JavaScript 代码就越大,保持源码短小并不总是一种选择。尽管下载一个大 JavaScript 文件只产生一次 HTTP 请求,却会锁定浏览器一大段时间。为避开这种情况,你需要向页面中逐步添加 JavaScript,某种程度上说不会阻塞浏览器。 非阻塞脚本非阻塞脚本的秘密在于,等页面完成加载之后,再加载JavaScript代码。实现方法:1.延迟脚本1<script src="file.js" defer></script> 这个defer属性指明元素中所包含的脚本不打算修改DOM,因此代码可以稍后执行(DOM加载完后执行)。(仅适用于IE4+和Firefox 3.5+) 2.动态脚本加载(最常用的模式)1234var script = document.createElement ("script");script.type = "text/javascript";script.src = "file1.js";document.getElementsByTagName("head")[0].appendChild(script); file1.js当元素<script>添加到页面之后立刻开始下载。此技术的重点在于:无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。跨浏览器实现动态加载js123456789101112131415161718function loadScript(url, callback){ var script = document.createElement ("script") script.type = "text/javascript"; if (script.readyState){ //IE浏览器 script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; callback(); //js文件接收完成时触发的回调函数 } }; } else { //其他浏览器 script.onload = function(){ callback(); //js文件接收完成时触发的回调函数 }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script);} 使用方法如下:123loadScript("file1.js", function(){ alert("File is loaded!");}); 在大多数浏览器中,不会保证文件加载的顺序,只有Firefox和Opera能够保证脚本按照你指定的顺序执行。可以采用串联的方式,保证下载次序:1234567loadScript("file1.js", function(){ loadScript("file2.js", function(){ loadScript("file3.js", function(){ alert("All files are loaded!"); }); });}); 3.XHR脚本注入使用XHR对象将脚本注入到页面中,继而实现非阻塞脚本。此技术首先创建一个XHR对象,然后下载JavaScript文件,接着用一个动态<script>元素将JavaScript代码注入页面。1234567891011121314var xhr = new XMLHttpRequest();xhr.open("get","file1.js",true);xhr.onreadystatechange = function(){ if(xhr.readyState == 4){ //2**表示有效的回应,304表示一个缓存响应 if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ var script = document.createElement("script"); script.type = "text/javascript"; script.text = xhr.responseText; // 创建内联代码的&lt;script&gt;元素 document.body.appendChild(script); } }}xhr.send(null); 这种方法的优点是:可以下载不立即执行的JavaScript代码;兼容性好,同样的代码在所有现代浏览器中都不会引发异常。 限制:JavaScript文件必须与页面放置在同一个域内,不能从CDN下载。(所以大型网页不采用这方法) 推荐的非阻塞模式推荐的向页面加载大量JavaScript的方法分为两个步骤: 第一步,包含动态加载的JavaScript所需的代码,然后加载页面初始化所需的除JavaScript之外的部分。这部分代码尽量小,它下载和运行非常迅速,不会对页面造成很大干扰。当初始代码准备好后,用它来加载其余的JavaScript代码。另外,也可以直接将加载其余JavaScript代码的函数嵌入在页面中,这样可以避免另一HTTP请求。典型示例是YUI3,核心设计理念是:用一个很小的初始代码,下载其余的功能代码。 非阻塞 JavaScript 加载库使用LazyLoad实现加载其他的功能代码(http://github.com/rgrove/lazyload/)可以下载多个JavaScript文件,并保证它们在所有浏览器上都能够按照正确的顺序执行。 总结几种减少JavaScript对性能的影响的方法:1)将所有<script>标签放置在页面的底部,紧靠body关闭标签</body>上方;2)将脚本成组打包。页面<script>标签越少,页面加载速度就越快,响应也更加迅速。 几种使用非阻塞方式下载JavaScript的方法:1)为<script>标签添加defer属性(仅适用于IE和Firefox3.5+)2)动态创建<script>元素,用它下载并执行代码3)用XHR对象下载代码,并注入到页面中 第二章 数据访问在JavaScript中有四种基本的数据访问位置:1.直接量直接量仅仅代表自己,而不存储于特定位置。JavaScript的直接量包括:字符串,数字,布尔值,对象,数组,函数,正则表达式,具有特殊意义的空值,以及未定义。 2.变量开发人员使用var关键字创建用于存储数据值 3.数组项具有数字索引,存储一个JavaScript数组对象。 4.对象成员具有字符串索引,存储一个JavaScript对象。 每一种数据存储位置都具有特定的读写操作负担。直接量和局部变量的访问速度要远大于数组项和对象成员的访问速度。 管理作用域作用域链和标识符解析每一个JavaScript函数都被表示为对象。进一步说,它是一个函数实例。函数对象如其他对象那样,拥有可以编程访问的属性,和一系列不能被程序访问,仅供JavaScript引擎使用的内部属性。其中一个内部属性就是[[Scope]]。内部[[Scope]]属性包含一个函数被创建的作用域中对象的集合。此集合被称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,它的作用域链被填充以对象,这些对象代表创建此函数的环境中可访问的数据。 在函数运行过程中,每遇到一个变量,标识符识别过程要决定从哪里获得或者存储数据。此过程搜索运行期上下文的作用域链,查找同名的标识符。 标识符识别性能在运行前上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。全局变量总是处于运行期上下文作用域链的最后一个位置,所以总是最远才能触及的。这也是为什么要尽量减少全局变量的原因之一。 在没有优化JavaScript引擎的浏览器中,最好尽可能使用局部变量。一个好的经验法则是:用局部变量存储本地范围之外的变量值,如果他们在函数中使用多于一次,考虑下面的例子:123456789101112131415function initUI(){ var bd = document.body, links = document.getElementsByTagname("a"), i=0, len = links,length; while(i<len){ update(links[i++]); } document.getElementById("go-btn").onclick = function(){ start(); }; bd.className = "active";} 以上代码中,包含了三个对document的引用。可以通过这种方法减轻重复的全局变量访问对性能的影响:首先将全局变量的引用存储在一个局部变量中,然后使用这个局部变量代替全局变量。 改变作用域链一般来说,一个运行前上下文的作用域链不会被改变。但是,有两种表达式可以在运行时临时改变运行期上下文作用域链。第一个是with表达式:12345678910111213141516function initUI(){ with(document){ // 应该避免使用 var bd = body, links = getElementsByTagname("a"), i=0, len = links,length; while(i<len){ update(links[i++]); } getElementById("go-btn").onclick = function(){ start(); }; bd.className = "active"; }} 重写后的initUI()版本使用了with()表达式,避免了多次书写“document”。看起来似乎更有效率,实际上却产生了一个性能问题: 当代码流执行到一个with表达式时,运行前上下文的作用域链被临时改变了。一个新的可变对象将被创建,它包括指定对象的所有属性。此对象被插入到作用域链的前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,所以访问代价更高了。 第二个是try-catch表达式的catch子句:当try块发生错误时,程序流程自动转入catch块,并将异常对象推入作用域链前端的一个可变对象中。12345try{ //some error}catch{ //alert error} 注意,当catch子句执行完毕,作用域链就会返回到原来的状态。若没有局部变量访问,作用域链临时改变就不会影响代码的性能。 动态作用域无论是with表达式还是try-catch表达式的catch子句,以及包含()的函数,都被认为是动态作用域。动态作用域只因代码运行而存在,因此无法通过静态分析(查看代码结构)来确定是否存在动态作用域。 闭包,作用域和内存闭包是JavaScript最强大的一个方面,它允许函数访问局部范围之外的数据,同时也能够使得变量常驻内存中,实现缓存。不过,有一个性能影响与闭包有关。由于闭包的[[Scope]]属性包含于运行前上下文作用域链相同的对象引用,会产生副作用。通常,一个函数的激活对象与运行期上下文一同销毁,当涉及闭包时,激活对象就无法销毁了,因为引用仍然存在于闭包[[Scope]]属性中,这就导致了闭包会需要更多的内存开销。 闭包最主要的性能关注点:经常访问一些范围之外的标识符,每次访问都导致一些性能损失。 小结:为了减轻对运行速度的影响,应将常用的域外变量存入局部变量中,然后直接访问局部变量。 对象成员大多数JavaScript代码以面向对象的形式编写。对象的一个命名成员可以包含任何数据类型。当一个命名成员引用了一个函数时,它被称作一个方法,而一个非函数的数据则被称为属性。 原型JavaScript中的对象是基于原型的。原型是其他对象的基础,定义并实现了一个新对象所必须具有的成员。原型对象为所有给定类型的对象实例所共享,因此所有实例共享原型对象的成员。一个对象通过一个内部属性绑定到它的原型,Firefox,Safari和Chrome支持这一属性:proto;其他浏览器不允许脚本访问这一属性。任何时候创建一个内置类型的实例,这些实例自动拥有一个Object作为它们的原型。 对象有两种成员:实例成员和原型成员。实例成员直接存在于实例自身,而原型成员则从对象原型继承。处理对象成员的过程:当对象的某个成员被调用时,对成员进行搜索,先从对象实例开始,如果没有找到,则转向搜索原型对象。 通过hasOwnProperty()函数确定一个对象是否具有特定名称的实例成员,若是为实例成员则返回true,若为原型成员则返回false。通过in操作符可以确定对象是否具有某个名称的成员,实例成员和原型成员都会被返回true。 原型链对象的原型决定了一个实例的类型。在原型链上搜索成员,每深入一层都会增加性能损失。 嵌套成员由于对象成员可能包含其他成员,例如:window.location.href。每遇到一个点号,JavaScript引擎就要在对象成员上执行一次解析过程。应尽量避免使用这种方法,同时在一个函数中不应该多次读取同一个对象成员的值,应将该对象成员的值保存在一个局部变量中。 总结JavaScript中有四种数据访问类型:直接量,变量,数组项,对象成员。直接量和局部变量访问速度非常快,数组项和对象成员需要更长时间。局部变量比域外变量快,因为局部变量位于作用域链的第一个对象中。全局变量总是最慢的,因为它们总是位于作用域链的最后一环。要避免使用with表达式和try-catch表达式的catch子句,因为它们改变了运行前上下文的作用域链。提高JavaScript代码性能的方法:将经常使用的对象成员,数组项和域外变量存入局部变量中,访问局部变量的速度回快于那些原始变量。 第三章 DOM编程对DOM操作代价昂贵,在富网页应用中通常是一个瓶颈。主要有三个方面:1.访问和修改DOM元素; 2.修改DOM元素的样式,造成重绘和重新排版; 3.通过DOM事件处理用户响应 DOM,即文档对象模型,是一个独立于语言的,使用XML和HTML文档操作的应用程序接口(API)。 访问和修改浏览器通常要求DOM实现和JavaScript实现保持相互独立。这样两个独立的部分以功能接口连接就会带来性能损耗。应尽量减少对DOM的操作次数,尽量保持在ECMAScript中。修改元素的代价更昂贵,因为它经常导致浏览器重新计算页面的几何变化。最坏的情况是使用循环执行访问或修改元素,特别是在HTML集合中使用循环。12345function innerHTMLLoop(){ for(var count = 0; count < 15000; count++){ document.getElementById("here").innerHTML += "a"; }} 修改如下,使用局部变量保存更新后的内容,在循环结束时一次性写入:1234567function innerHTMLLoop(){ var content = ""; for(var count = 0; count < 15000; count++){ content +="a"; } document.getElementById("here").innerHTML += content;} innerHTML 与 DOM方法比较两者性能相差不大,但若要更新一大块HTML页面,innerHTML在大多数浏览器中执行更快。 节点克隆使用DOM方法更新页面内容的另一个途径是克隆已有DOM元素,而不是创建新的,即使用element.cloneNode()代替document.createElement()。用克隆节点的方法重新生成一个表格,单元格只创建一次,然后重复执行复制操作,这样做能够稍微快了一点。 HTML集合HTML集合是用于存放DOM节点引用的类数组对象。下列函数返回的值就是一个集合:document.getElementsByTagName();document.getElementsByClassName();//当下大多数浏览器都不能使用 下列属性也属于HTML集合:document.images 页面中所有的<img>元素document.links 页面中所有的<a>元素document.forms 页面中所有的<form>表单document.forms[0].elements 页面中第一个表单的所有字段 HTML集合是一种类似数组的列表,但不是数组,没有数组的push()或slice()之类的方法,但提供了length属性。也可以和数组一样通过下标索引访问列表中的元素。HTML集合实际上在查询文档,当你更新信息时,每次都要重复执行查询操作,效率很低。在集合遍历或数组遍历时,将length属性存入一个变量中,从而提升运行速度。 访问集合元素时使用局部变量一般来说,对于任何类型的DOM访问,如果同一个DOM属性或方法被访问一次以上,最好使用一个局部变量缓存此DOM成员。当遍历一个集合时,第一个优化是将集合引用存储于局部变量,并在循环之外缓存length属性。然后如果在循环体中多次访问同一个集合元素,那么使用局部变量缓存它。12345678910111213141516171819202122232425262728293031323334353637383940// slowfunction collectionGlobal() { var coll = document.getElementsByTagName('div'), len = coll.length, name = ''; for (var count = 0; count < len; count++) { name = document.getElementsByTagName('div')[count].nodeName; name = document.getElementsByTagName('div')[count].nodeType; name = document.getElementsByTagName('div')[count].tagName; } return name;};// fasterfunction collectionLocal() { var coll = document.getElementsByTagName('div'), len = coll.length, name = ''; for (var count = 0; count < len; count++) { name = coll[count].nodeName; name = coll[count].nodeType; name = coll[count].tagName; } return name;};// fastestfunction collectionNodesLocal() { var coll = document.getElementsByTagName('div'), len = coll.length, name = '', el = null; for (var count = 0; count < len; count++) { el = coll[count]; name = el.nodeName; name = el.nodeType; name = el.tagName; } return name;}; DOM漫谈抓取DOM经常需要从一个DOM元素开始,操作周围的元素,或者递归迭代所有的子节点。使用childNodes集合或者使用nextSibling获得每个元素的兄弟节点。123456789101112131415161718function testNextSibling(){ var el = document.getElementById("myDiv"); var child = el.firstChild,name =""; do{ name = ch.nodeName; }while(ch=ch.nexSibling) return name;}function testChildNodes(){ var el = document.getElementById("myDiv"), ch = el.childNodes, len = ch.length,name=""; for(var count = 0;count < len; count++){ name = ch[count].nodeName; } return name;} 在不同浏览器上,nextSibling和childNodes方法的运行时间基本相等,但在IE中,nextSibling比childNodes好。所以在IE中,用nextSibling更好,其他浏览器随意。 元素节点DOM属性诸如childNodes,firstChild和nextSibling不区分元素节点和其他类型节点,如注释节点和文本节点。在多数情况下,只有元素节点需要被访问,所以在循环中,应对节点返回类型进行检查,过滤出非元素节点。但这些检查和过滤不是必要的,许多浏览器提供了API函数只返回元素节点。 选择器API最新的浏览器提供了querySelectorAll()的原生浏览器DOM函数。该方法比使用JavaScript和DOM迭代并缩小元素列表的方法要快。123var elements = document.querySelectorAll("#menu a");querySelectorAll() 接收一个CSS选择器字符串参数并返回一个NodeList——由符合条件的节点构成的类数组对象。还可以通过querySelectorAll()一次性获得多类的节点,进行联合查询:12//获取类名为warning和notice的div元素var errs = document.querySelectorAll("div.warning,div.notice"); 另外还有一个querySelector()选择器API。该函数只返回符合查询条件的第一个节点。 重绘和重排版当浏览器下载完所有HTML标记,JavaScript,CSS,图片之后,它解析文件并创建两个内部数据结构:一棵DOM树:表示页面结构;一棵渲染树:表示DOM节点如何显示。 一旦DOM树和渲染树构造完毕,浏览器就可以显示页面上的元素了。 当DOM改变影响到元素的几何属性(宽和高),浏览器需要重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此改变受到影响。浏览器使渲染树上受影响的部分失效,然后重构渲染树。这个过程被称作重排版。 当只改变DOM的样式时(没有改变页面布局),在这种情况下,只需要重绘。 重排版会发生什么? 除了当布局和几何改变时需要重排版,下列情况也需要重排版:添加或删除可见的DOM元素;元素位置改变;元素尺寸改变;内容改变,例如文本改变或图片被另一个不同尺寸的所替代。最初的页面渲染;浏览器窗口改变尺寸。 最小化重绘和重排版重绘和重排版代价昂贵,所以,提高程序响应速度一个好策略是减少此类操作发生的机会。为减少发生次数,应该将多个DOM和风格改变合并到一个批次中一次性执行。当需要改变一个元素多个样式属性时,应该使用cssText()方法12var el = document.getElementById('mydiv');el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;'; 以上代码只修改DOM一次,提高了效率。这个例子中的代码修改 cssText 属性,覆盖已存在的风格信息。如果你打算保持当前的风格,你可以将它附加在 cssText 字符串的后面。1el.style.cssText += '; border-left: 1px;'; 批量修改DOM当你需要对DOM元素进行多次修改时,可以通过以下步骤减少重绘和重排版的次数: 从文档流中摘除该元素; 对其应用多重改变; 将元素带回文档中; 此过程只引发了两次重排版——第一步和第三步。若忽略这两个步骤,则第二步中每次改变都将引发一次重排版。 有三种方法可以将DOM从文档中摘除:1)隐藏元素,进行修改,然后再显示它;2)使用一个文档片段在已存DOM之外创建一个子树,然后将它拷贝到文档中;(性能较优)3)将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素。 缓冲布局信息浏览器通过队列化修改和批量运行的方法,尽量减少重排版次数。当你查询布局信息如偏移量、滚动条位置,或风格属性时,浏览器刷新队列并执行所有修改操作,以返回最新的数值。最好是尽量减少对布局信息的查询次数,查询时将它赋给局部变量,并用局部变量参与计算。1234567var current = myElement.offsetLeft; //只查询了布局信息一次current++myElement.style.left = current + 'px';myElement.style.top = current + 'px';if (current >= 500) { stopAnimation();} 将元素提出动画流显示和隐藏部分页面构成页面展开/折叠动画是一种常见的交互模式。它通常包括区域扩大的几何动画,将页面其他部分推向下方。浏览器需要重排版的部分越小,应用程序的响应速度就越快。而当一个页面顶部的动画推移了差不多整个页面时,将引发巨大的重排版动作,使用户感到动画卡顿。渲染树的大多数节点需要被重新计算,它变得更糟糕。 使用以下步骤可以避免大部分页面进行重排版:1)使用绝对坐标定位页面动画的元素,使它位于页面布局流之外;2)启动动画元素。当它扩大时,它临时覆盖部分页面。这是一个重绘的过程,但只影响页面的一小部分,避免重排版并重绘一大块动画3)当动画结束,重新定位,从而只一次下移文档其他元素的位置。 译者注:文字描述比较简单概要,我对这三步的理解如下:1、页面顶部可以“折叠/展开”的元素称作“动画元素”,用绝对坐标对它进行定位,当它的尺寸改变时,就不会推移页面中其他元素的位置,而只是覆盖其他元素。2、 展开动作只在“动画元素”上进行。 这时其他元素的坐标并没有改变, 换句话说, 其他元素并没有因为“动画元素”的扩大而随之下移,而是任由动画元素覆盖。3、“动画元素”的动画结束时,将其他元素的位置下移到动画元素下方,界面“跳”了一下。 IE和 :hover如果大量使用了伪类:hover,那么会降低反应速度。此问题在IE8中尤为显著。 事件托管(事件委托)当页面中存在大量元素,而且每个元素有一个或多个事件句柄与之挂接(例如onclick)时,可能会影响性能。连接每个句柄都是有代价的。挂接事件占用了处理时间,另外,浏览器需要保存每个句柄的记录,占用更多内存。事件托管基于这样一个事实:事件逐层冒泡总能被父元素捕获。采用事件托管技术之后,你只需要在一个包装元素上挂接一个句柄,用于处理子元素发生的所有事件。DOM事件的三个阶段:捕获阶段,目标阶段,冒泡阶段 总结将DOM和ECMAScript比作两座岛,每次操作DOM都要收取“过路费”,因此为减少DOM编程中的性能损失,记住以下几点:1)最小化DOM访问,尽量在JavaScript端做尽可能多的事情;2)在反复访问的地方使用局部变量存放DOM引用;3)小心地处理 HTML 集合,因为他们表现出“存在性”,总是对底层文档重新查询。将集合的 length 属性缓存到一个变量中,在迭代中使用这个变量。如果经常操作这个集合,可以将集合拷贝到数组中;4)在浏览器允许的情况下,尽可能使用速度更快的API,如querySelectorAll和firstElementChild等;5)注意重绘和重排版;批量修改风格(cssText),离线操作DOM,缓存并减少对布局信息的访问;6)动画中使用绝对定位,使用拖放代理7)使用事件托管技术最小化事件句柄数量]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>高性能JavaScript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(九)]]></title>
<url>%2F2017%2F05%2F18%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89%2F</url>
<content type="text"><![CDATA[JavaScript高级技巧1.高级函数(操作函数的函数)安全的类型检测Javascript内置的类型检测机制并非完全可靠,常见的有typeof和instanceof。1var isArray = value instanceof Array; 以上语句要返回true,value必须是一个数组,而且还必须与Array构造函数在同个全局作用域中。在一个页面嵌入多个frame的情况下,如果value是在另个frame中定义的数组,那么以上语句返回false。 解决上述问题的办法是:在任何值上调用Oject原生的toString()方法,都会返回一个[object NativeConstructorName]格式字符串。1alert(Object.prototype.toString.call(value)); //"[object Array]" Object的toString()方法不能检测非原生构造函数,因为自定义的任何构造函数都将返回[object Object](此方法并非完全可靠,因为Object.prototype.toString()方法也可能被修改) 作用域安全的构造函数构造函数其实就是一个使用new操作符调用的函数。当new调用时,构造函数内用到的this对象会指向新创建的对象实例。作用域安全的构造函数在进行任何更改前,首先确认this对象是正确类型的实例。如果不是,那么会创建新的实例并返回。123456789function Person(name, age, job){ if(this instanceof Person){ this.name = name; this.age = age; this.job = job; }else{ return new Person(name,age,job); }} 实现上述的模式后,你就锁定了可以调用构造函数的环境。如果你使用构造函数窃取模式的继承且不使用原型链,那么这个继承很可能被破坏。(构造函数窃取模式是常见的一种实现JavaScript继承的方法,做法是在”子类“的构造函数中调用父类的构造函数以实现继承父类属性。)123456789101112131415161718192021function Polygon(sides){ if(this instanceof Polygon){ this.sides = sides; this.getArea = function(){ return 0; } }else{ return new Polygon(sides); }}function Rectangle(width,height){ Polygon.call(this,2);//构造函数窃取模式 this.width = width; this.height = height; this.getArea = function(){ return this.width * this.height; }}var rect = new Rectangle(5,10);alert(rect.sides); //undefined Polygon构造函数是作用域安全的,但Rectangle构造函数不是。Rectangle构造函数中的this并没有得到增长,通过Polygon.call()返回的值没有用到,所以Rectangle实例中不会有sides属性。 解决方法:构造函数窃取结合使用原型链或者寄生组合123456789101112131415161718192021222324function Polygon(sides){ if(this instanceof Polygon){ this.sides = sides; this.getArea = function(){ return 0; } }else{ return new Polygon(sides); }}function Rectangle(width,height){ Polygon.call(this,2); this.width = width; this.height = height; this.getArea = function(){ return this.width * this.height; }}Rectangle.prototype = new Polygon();var rect = new Rectangle(5,10);alert(rect.sides); //2 惰性载入函数惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式:1)在函数被调用时再处理函数在第一次调用过程中,该函数会被覆盖为另一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。以创建XHR对象的createXHR()函数为例1234567891011121314151617181920212223242526272829function createXHR(){ if(typeof XMLHttpRequest != "undefined"){ createXHR = function(){ return new XMLHttpRequest(); }; }else if(typeof ActiveXObject != "undefined"){ createXHR =function(){ if(typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], i,len; for(i=0,len=versions.length; i < len; i++){ try{ new ActiveXObject(version[i]); arguments.callee.activeXString = versions[i]; break; }catch(ex){ //... } } } return new ActiveXObject(arguments.callee.activeXString); }; }else{ createXHR = function(){ throw new Error("No XHR object available."); }; } return createXHR;} 在这个惰性载入的createXHR()中,if语句的每一个分支都会为createXHR变量赋值,有效覆盖了原有的函数,最后一步就是调用新赋的函数,下次调用createXHR()时候,就会直接调用被分配的函数。 2)在声明函数时就指定适当的函数第一次调用函数时不会损失性能,而在代码首次加载时会损失一点性能。12345678910111213141516171819202122232425262728var createXHR = (function createXHR(){ if(typeof XMLHttpRequest != "undefined"){ return function(){ return new XMLHttpRequest(); }; }else if(typeof ActiveXObject != "undefined"){ return function(){ if(typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], i,len; for(i = 0,len = versions.length; i < len; i++){ try{ new ActiveXObject(version[i]); arguments.callee.activeXString = versions[i]; break; }catch(ex){ //... } } } return new ActiveXObject(arguments.callee.activeXString); }; }else{ return function(){ throw new Error("No XHR object available."); }; }})(); 这种方法的技巧是创建一个匿名、自执行的函数,用以确定应该使用哪一个函数实现。惰性载入函数的优点是只在执行分支代码时牺牲一点性能。 函数绑定函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。12345678var handler ={ message : "Event handled", handleClick : function(event){ alert(this.message); }};var btn = document.getElementById("my-btn");EventUtil.addHandler(btn,"click",handler.handleClick); 结果输出:undefined。因为并没有保存handler.handleClick()的环境,所以this对象最后是指向了DOM按钮(btn)而非handler(IE8中this指向window)。 使用闭包来修正这个问题:12345678910var handler ={ message : "Event handled", handleClick : function(event){ alert(this.message); }};var btn = document.getElementById("my-btn");EventUtil.addHandler(btn,"click",function(event){ handler.handleClick(event);}); 在onclick事件处理程序中是用来闭包来直接调用handler.handleClick(),结果为显示Event handled。很多JavaScript库中实现了一个可以将函数绑定到指定环境的函数,bind()。12345function bind(fn, context){ return function(){ return fn.apply(context,arguments); };} 在bind()中创建了一个闭包,闭包使用apply()调用传入的函数,并给apply()传递context对象和参数。用法:12345678var handler ={ message : "Event handled", handleClick : function(event){ alert(this.message); }};var btn = document.getElementById("my-btn");EventUtil.addHandler(btn,"click",bind(handler.handleClick,handler)); 在ECMAScript5为所有的函数定义了一个原生的bind()方法,可以直接在函数上调用这个方法,进一步简化了操作。1EventUtil.addHandler(btn,"click",handler.handleClick.bind(handler)); 支持原生bind()方法的浏览器有IE9+、Firefox4+和Chrome。 函数柯里化与函数绑定紧密相关的主题是函数柯里化,它用于创建已经设置好了一个或多个参数的函数。 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 通俗点讲,函数柯里化就是把函数完全变成「接受一个参数;返回一个值」的固定形式。 函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。12345678910function add(num1, num2){ return num1 + num2;}function curriedAdd(num2){ return add(4, num2);}alert(add(2, 3)); // 5alert(curriedAdd(3)); // 7 curriedAdd()函数本质上是在任何情况下第一个参数为4的add()版本,尽管从技术上说curriedAdd()并非柯里化函数,但它很好地展示了其概念。柯里化函数通常由以下步骤动态创建:调用一个函数并为它传入要柯里化的函数和必要的参数。创建柯里化函数的通用方式:123456789function curry(fn){ var args = Array.prototype.slice.call(arguments,1); // 这个arguments是外部的,即curry的参数 return function(){ // 这个arguments是内部的,即curryAdd的参数 var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); // 连接两个或多个数组 return fn.apply(null,finalArgs); };} 这个函数并没有考虑到执行环境,所以调用apply时第一个参数是null。123456function add(num1, num2){ return num1 + num2;}var curriedAdd = curry(add, 5); // 返回已设置第一个参数为5的函数alert(curriedAdd(3)); // 8 结合函数柯里化的更复杂的bind()函数12345678function bind(fn,context){ var args = Array.prototype.slice.call(arguments,2); //给被绑定的函数的参数是从第三个开始 return function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(context,finalArgs); };} 可传入任意参数的,不传入参数时输出结果的柯里化函数:123456789101112131415161718192021var adder = function(){ var _args = []; return function(){ if(arguments.length == 0){ //不传入参数时,输入累加结果 return _args.reduce(function(a,b){ return a+b; }); /* return fn.apply(this,_args);//当有传入处理函数时,可根据处理函数对数据进行相关操作 */ } [].push.apply(_args,[].slice.call(arguments)); return arguments.callee(); }}var sum = adder();sum(100,200)(300);sum(400);console.log(sum()); //1000 防篡改对象因为在JavaScript中,任何对象都可以被同一环境中运行的代码修改,开发人员很可能意外地修改别人的代码,因此ECMAScript5提出了防篡改对象。 1.不可扩展对象默认情况下,JavaScript的所有对象都是可以扩展的,也就是说,任何时候都可以向对象中添加属性和方法。要想改变这一行为,需要使用Object.preventExtensions(object)方法,让你不能再给对象添加属性和方法,但可以修改删除已有的属性和方法。1234var person = {name : Peter};Object.preventsions(person);person.age = 22;console.log(person.age); //undefined 使用Object.isExtensible(object)方法可以确定对象是否可以扩展。 2.密封的对象ECMAScript 5为对象定义的第二个保护级别是密封对象。密封对象不可扩展,已有属性和方法不可删除和修改。使用Object.seal()方法密封对象:12345var person = {name: "Peter"};Object.seal(person);person.name = "Bob";alert(person.name); //"Peter" 使用Object.isSeal()方法判断对象是否被密封,因为被密封的对象不可扩展,所以用Object.isExtensible()检查密封对象也会返回false。 3.冻结的对象最严格的防篡改级别是冻结对象。冻结对象即不可扩展,又是密封的,而且对象数据属性的[[Writable]]特性会被设置为false。如果定义[[Set]]函数,访问器属性仍然是可写的。使用Object.freeze()方法冻结对象:12345678var person ={name : "Peter"};Object.freeze(person);person.age = 29;alert(person.age); // "undefined"delete person.name;alert(person.name); // "Peter" 使用Object.isForzen()检测冻结对象,而使用Object.isSeal()和Object.isExtensible()检测冻结对象,分别返回true和false。 高级定时器 Javascript是运行于单线程的环境中的,而定时器仅仅只是计划代码在未来的某个时间执行。执行时机是不能保证的,因为在页面的生命周期中,不同时间可能有其他代码在控制JavaScript进程。实际上,是由浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。可将JavaScript想象成在时间线上运行,以onclick事件为例: 定时器对队列的工作方式是,当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它立刻执行,而只能表示它会尽快执行。关于定时器要记住的最重要的事情是:指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。 重复的定时器使用setInterval()创建的定时器确保了定时器代码规则地插入队列中。当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。重复定时器的规则有两个问题:(1)某些间隔会被跳过;(2)多个定时器的代码执行之间的间隔可能会比预期的小。 为了避免setInterval()的重复定时器的这两个缺点,可以如下模式使用链式setTimeout()调用:12345setTimeout(function(){ //处理中 setTimeout(argument.callee, interval);}, interval); 这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔,同时也能避免连续的运行。 Yielding Processes运行于浏览器中的Javascript都被分配了一个确定数量的资源。如果代码运行超过特定的时间或者特定语句数量就不让它继续执行。定时器就是绕开此限制的方法之一。 脚本长时间运行的问题通常由两个原因之一造成的:过长的、过深嵌套的函数调用或者是进行大量处理的循环。对于由进行大量处理的循环导致的脚本长时间运行问题,是较容易解决的,首先确定两个问题:1)该处理是否必须同步完成?2)数据是否必须按顺序完成? 如果回答都为“否”,那么可以使用定时器分割这个循环:这是一种叫做数组分块的技术,小块小块地处理数组,通常每次一小块。 基本思路是:为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。基本模式:12345678910setTimeout(function(){ //取出下一个条目并处理 var item = array.shift(); process(item); //若还有条目,再设置另一个定时器 if(array.length > 0){ setTimeout(argument.callee, 100); }}, 100); 具体函数:123456789101112function chunk(array,process,context){ setTimeout(function(){ //取出下一个条目并处理 var item = array.shift(); process.call(context,item); //若还有条目,再设置另一个定时器 if(array.length>0){ setTimeout(arguments.callee,100); } },100); } 应注意的是,传给chunk()函数的数组是用作一个队列的,因此当处理数据时,数组中的条目也相应变化, 如果想要保持原数组不变,则应该将该数组的克隆传递给chunk(),如下所示:1chunk(data.concat(),printValue); 数据分块的重要性在于他它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其他的浏览器处理机会运行,这样就可能避免长时间运行脚本的错误。 函数节流 函数节流的基本思想是指,某些代码不可以在没有间断的情况下连续重复执行。第一次调用函数,创建一个定时器,在制定的时间间隔之后运行代码。第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有意义。然而,如果前一个定时器尚未中心,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。基本形式:1234567891011121314151617var processor = { timeoutId : null, //实际进行处理的方法 perfoemProcessing : function(){ //实际执行的代码 },//初始处理调用方法 process : function(){ clearTimeout(this.timeoutId); var that = this; this.timeoutId = setTimeout(function{ that.performProcessing(); },100); }};//尝试开始执行processor.process(); 只要代码是周期性执行的,都应该使用节流。 自定义事件事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。另一方面,观察者知道主体并能注册事件的回调函数(事件处理程序)。 事件是与DOM交互的最常见的方式,但也可以用于非DOM代码中——通过实现自定义事件。自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听这些事件。基本模式如下:1234567891011121314151617181920212223242526272829303132333435363738function EventTarget(){ this.handlers = {};}EventTarget.prototype = { constructor : EventTarget, //注册某个事件类型的事件处理程序 addHandler : function(type,handler){ if(typeof this.handlers[type] == "undefined"){ this.handlers[type] = []; } this.handlers[type].push(handler); }, //触发事件 fire : function(event){ if(!event.target){ event.target = this; } if(this.handlers[event.type] instanceof Array){ var handlers = this.handlers[event.type]; for(var i = 0,len = handlers.length; i < len; i++){ handlers[i](event); } } }, //注销某个事件类型的事件处理程序 removeHandler : function(type, handler){ if(this.handlers[type] instanceof Array){ var handlers = this.handlers[type]; for(var i=0,len=handlers.length; i< len;i++){ if(handlers[i] === handler){ break; } } handlers.splice(i,1); } }}; 使用自定义事件有助于解耦相关对象,保持功能的隔绝。 拖放基本概念:创建一个绝对定位的元素,使其可以用鼠标移动。这个技术源自一种叫做“鼠标拖尾”的经典网页技巧。鼠标拖尾是一个或者多个图片在页面上跟着鼠标指针移动。12345EventUtil.addHandler(document,"mousemove",function(event){ var myDiv = document.getElementById("myDiv"); myDiv.style.left = event.clientX+"px"; myDiv.style.top = event.clientY+"px";}); 拖放示例:12345678910111213141516171819202122232425262728// 拖拽对象var ie=document.all;var nn6=document.getElementById&&!document.all;//判断是否是IE浏览器,ture表示不是,false表示是IE浏览器。var dragFlag=false, mouseY, mouseX, dragObj, objX,objY;$(function(){ dragObj=document.getElementById("block"); dragObj.style.top=0; dragObj.style.left=0; dragObj.onmousedown=function(e){ //获取鼠标坐标和拖拽目标的坐标 dragFlag=true; mouseX=parseInt(nn6 ? e.clientX : event.clientX);//鼠标位置的x坐标 mouseY=parseInt(nn6 ? e.clientY : event.clientY);//鼠标位置的y坐标 objX=parseInt(dragObj.style.left); objY=parseInt(dragObj.style.top); return false; } dragObj.onmousemove=function(e){ if(dragFlag==true){ //计算拖拽目标移动的位置坐标 dragObj.style.top = (nn6 ? objY + e.clientY - mouseY : objY+ event.clientY - mouseY)+"px"; dragObj.style.left = (nn6 ? objX + e.clientX - mouseX : objX + event.clientX - mouseX)+"px"; return false; } } dragObj.onmouseup=new Function("dragFlag=false"); dragObj.onmouseleave=new Function("dragFlag=false");}); 离线应用与客户端存储HTML5把离线应用作为重点,开发人员都期望web应用能够离线使用。 开发离线Web应用需要几个步骤: 确保应用知道设备是否能上网,以便下一步执行正确的操作。 应用必须能访问一定的资源(图像,Javascript,CSS等)。 最后,必须有一块本地空间用于保存数据,无论能否上网都不妨碍读写。(HTML5及其相关的API让开发离线应用成为现实) 1.离线检测为了实现检测是否离线,HTML5定义了一个navigator.onLine属性,该属性值为true表示设备能上网,值为false表示设备离线。这个属性的关键在于浏览器要必须知道设备能否访问网络,从而返回正确的值。在实际应用中,navigator.onLine在不同浏览器间有些小差异: 由于存在以上兼容性问题,单独使用navigator.onLine属性并不能确定网络是否连通,所以为了更好地确定网络是否可用,HTML5还定义了两个事件:online和offline。当网络从离线变为在线或者从在线变为离线时,分别触发这两个事件。这两个事件都是在window对象上触发。123456EventUtil.addHandler(window,"online",function(){ alert("Online");});EventUtil.addHandler(window,"offline",function(){ alert("Offline");}); 2.应用缓存HTML5的应用缓存,或者简称appcache,是专门为开发离线Web应用而设计的。Appcache就是从浏览器的缓存中分出来的一块缓存区。要想在这个缓存中保存数据,可以使用一个描述文件(manifest file),列出要下载和缓存的资源。描述文件示例12345CACHE MANIFEST#Commentfile.jsfile.css 要将描述文件与页面关联起来,可以在<html>中的manifest属性中指定这个文件的路径,例如:1<html manifest="/offline.manifest"> 通过Javascript API能够知道缓存的行为,这个API的核心是applicationCache对象,这个对象有一个status属性,属性的值是常量,表示应用缓存的如下当前状态:0:无缓存,即没有与页面相关的应用缓存。1:闲置,即应用缓存未得到更新。2:检查中,即正在下载描述文件并检查更新。3:下载中,即应用缓存正在下载描述文件中指定的资源。4:更新完成,即应用缓存已经更新了资源,而且所有资源都已下载完毕,可以通过swapCache()来使用了。5:废弃,即应用缓存的描述文件已经不存在了,因此页面无法再访问应用缓存。 与缓存相关的事件:checking:在浏览器为应用缓存查找更新时触发。error:在检查更新或下载资源期间发生错误时触发。noupdate:在检查描述文件发现文件无变化时触发。downloading:在开始下载应用缓存的过程中持续不断地触发。updateready:在页面新的应用缓存下载完毕且可以通过swapCache()使用时触发。cached:在应用缓存完整可用时触发。 此外,通过调用update()方法可以手工干预,让应用缓存为检查更新而触发上述事件。 3.数据存储1)CookieHTTP Cookie,通常直接叫做cookie,最初是在客户端用于存储会话信息的。 限制cookie在性质上是绑定在特定的域名下的。每个域的cookie的总数是有限的,不过浏览器之间各有不同。IE6以及更低版本限制每个域名只能20个cookie;IE7+和Firefox最多50个;Opera最多30个;Safari和Chrome对于每个域的cookie数量限制没有硬性规定。 cookie大小也有限制,最多有4096B,为了最佳的浏览器兼容性,最好将整个cookie长度限制在4095B。 cookie的构成名称:一个唯一确定cookie的名称(不区分大小写,必须经过URL编码)。值:储存在cookie中的字符串值。值必须被URL编码。域:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含整个cookie信息。路径:对于指定域中的那个路径,应该向服务器发送cookie。失效时间:表示cookie何时应该被删除的时间戳。默认是浏览器会话结束时即将所有cookie删除,不过也可以自己设置删除时间。安全标志:指定后,cookie只有在使用SSL连接的时候才发送到服务器。 JavaScript中的cookie在JavaScript中处理cookie有些复杂,因为BOM的document.cookie属性不太容易处理。通过document.cookie返回当前页面可用的所有cookie的字符串,一系列由分号隔开的名值对儿:1name1=value1;name2=value2;name3=value3;]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(八)]]></title>
<url>%2F2017%2F05%2F17%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AB%EF%BC%89%2F</url>
<content type="text"><![CDATA[Ajax与Comet1.XMLHttpRequest对象IE5是第一款引入XHR对象的浏览器,但XHR对象是通过MSXML库中的一个ActiveX对象实现的。123456789101112131415161718//适用于IE7之前的版本function createXHR(){ if(typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], i,len; for(i = 0,len = versions.length; i < len; i++){ try{ new ActiveXObject(version[i]); arguments.callee.activeXString = versions[i]; break; }catch(ex){ //... } } return new ActiveXObject(arguments.callee.activeXString); } return new ActiveXObject(arguments.callee.activeXString);} 兼容各个浏览器1234567891011121314151617181920212223function createXHR(){ if(typeof XMLHttpRequest != "undefined"){ return new XMLHttpRequest(); }else if(typeof ActiveXObject != "undefined") if(typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], i,len; for(i = 0,len = versions.length; i < len; i++){ try{ new ActiveXObject(version[i]); arguments.callee.activeXString = versions[i]; break; }catch(ex){ //... } } } return new ActiveXObject(arguments.callee.activeXString); }else{ throw new Error("No XHR object available."); }} 2.XHR的用法在使用XHR对象时,调用的一个方法是open(),它接收三个参数:要发送的请求的类型(“get”、“post”等)、请求的URL和表示是否异步发送请求的布尔值。1xhr.open("get","example.php",false); 如果要发送特定的请求,必须使用send()方法:12xhr.open("get","example.php",false);xhr.send(null); send()方法接收一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入null. 在收到响应后,响应的数据会自动填充XHR对象的属性:responseText: 作为响应主体被返回的文本。responseXML: 如果响应的内容类型是“text/xml”或“application/xml”,这个属性中将保存包含着响应数据的XML DOM文档。status: 响应的HTTP状态。statusText: HTTP状态的说明。 在接收到响应后,第一步是检查status属性,以确定响应已成功返回。状态码为304表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本。12345678xhr.open("get","example.php",false);xhr.send(null);if(xhr.status >=200 &&xhr.status <300 ||xhr.status==304){ alert(xhr.responseText);}else{ alert("Request was unsuccessful:"+xhr.status);} 在多数情况下,我们需要发送异步请求,才能让Javascript继续执行而不必等待响应。所以需要检测XHR对象的readyState属性,该属性表示请求/响应过程的当前活动阶段,有以下值:0:未初始化。尚未调用open()方法。1:启动。已经调用open()方法,但尚未调用send()方法2:发送。已经调用send()方法,但尚未接收到响应3:接收。已经接收部分响应数据。4:完成。已经接收到全部响应数据,而且已经可以在客户端使用。 当readyState值改变时,将触发readyStatechange事件。12345678910var xhr = createXHR();xhr.onreadyStatechange = function(){ if(xhr.readyState == 4){ alert(xhr.responseText); }else{ alert("Request was unsuccessful:"+xhr.status); }}xhr.open("get","example.php",true);xhr.send(null); 在接收到响应之前还可以调用abort()方法来取消异步请求:xhr.abort()。 3.HTTP头部信息Accept:浏览器能够处理的内容类型Accept-Charset:浏览器能够显示的字符集Accept-Encoding:浏览器能够处理的压缩编码Accept-Language:浏览器当前设置的语言Connection:浏览器与服务器之间连接的类型Cookie:当前页面设置的任何CookieHost:发出请求的页面所在的域Referer:发出请求的页面URLUser-Agent:浏览器的用户代理字符串 可以使用setRequestHeader()方法设置自定义的请求头部信息。这个方法接收两个参数:头部字段的名称和头部字段的值。要想成功发送请求头部信息,必须在调用open()方法之后且调用send()方法之前调用setRequestHeader()。 获取头部信息,可以使用XHR对象的getResponseHeader()方法并传入头部字段名称,可以取得相应的响应头部信息。而调用getAllResponseHeaders()方法则可以取得一个包含所有头部信息的长字符串。 4.GET请求GET是最常见的请求类型,最常用于向服务器查询某些信息。使用GET请求经常发生的一个错误,就是查询字符串的格式有问题。下面这个函数可以辅助向现有URL的末尾添加查询字符串参数:12345function addURLParam(url, name, value){ url += (url.indexOf("?") == -1 ? "?" : "&"); url += encodeURIComponent(name)+ "=" + encodeURIComponent(value); return url;} 5.POST请求通常用于向服务器发送应该被保存的数据。 XMLHttpRequest 2级1.FormData为了实现表单数据的序列化,XMLHttpRequest 2级为此定义了FromData类型。FormData为序列化表单以及创建与表单格式相同的数据(用于通过XHR传输)提供了便利。12var data = new FormData();data.append("name","Peter"); 创建了一个FormData表单对象,通过append()方法向表单中传入表单字段的名字和字段中包含的值。也可以通过向FormData构造函数中传入表单元素。1234567891011var xhr = createXHR();xhr.onreadyStatechange = function(){ if(xhr.readyState == 4){ alert(xhr.responseText); }else{ alert("Request was unsuccessful:"+xhr.status); }}xhr.open("post","postexample.php",true);var form = document.getElementById("user-info");xhr.send(new FormData(form)); 2.超时设定(仅仅IE8支持)IE8为XHR对象添加了一个timeout属性,表示请求在等待响应多少毫秒之后就终止。在给timeout设置一个数值后,如果在规定时间内浏览器还没有接收到响应,那么就会触发timeout事件,进而调用ontimeout事件处理程序。1234567891011121314151617181920var xhr = createXHR();xhr.onreadyStatechange = function(){ if(xhr.readyState == 4){ try{ if(xhr.status >=200 &&xhr.status <300 ||xhr.status==304){ alert(xhr.responseText); }else{ alert("Request was unsuccessful:"+xhr.status); } }catch(ex){ } }};xhr.open("get","example.php",true);xhr.timeout = 1000;xhr.ontimeout = function(){ alert("Request did not return in a second.");}xhr.send(null); 如果在超时终止请求之后再访问status属性,会导致错误,所以要将检查status属性的语句放在try-catch中。 3.overrideMimeType()方法firefox最早引入overrideMimeType()方法,用于重写XHR响应的MIME类型。 进度事件Progress Events规范是W3C的一个工作草案,定义了与客户端服务器通信有关的事件。有以下6个进度事件:loadstart:在接收到响应数据的第一个字节时触发。progress:在接收响应期间持续不断地触发。(必须在调用open()方法之前添加onprogress()事件处理程序)error:在请求发生错误时触发。abort:在因为调用abort()方法而终止连接时触发。load:在接收到完整的响应数据时触发。loadend:在通信完成或者触发error、abort或load事件后触发。 跨域资源共享(CORS)CORS背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。CSRF:跨域请求伪造XSS:跨站点脚本 IE对CORS实现主要是通过XDR对象来实现,XDR对象与XHR类似,所有XDR请求都是异步的。1var xdr = new XDomainRequest(); CORS通过一种叫做Prefighted Request的透明服务器验证机制支持开发人员使用自定义的头部、GET或POST之外的方法,以及不同类型主体内容。 检测XHR是否支持CORS的最简单方式,就是检查是否存在withCredentials属性 跨浏览器的CORS实现:1234567891011121314151617181920function createCORSRequest(method, url){ var xhr = new XMLHttpRequest(); if("withCredentials" in xhr){ xhr.open(method,url,true); }else if(typeof XDomainRequest != "undefined"){ xhr = new XDomainRequest(); xhr.open(method,url); }else{ xhr = null; } return xhr;}var request = createCORSRequest("get","http://www.somewhere-else.com/page/");if(request){ request.onload = function(){ //对request.responseText 进行处理 } request.send();} 其他跨域技术1.图像Ping第一种跨域技术是使用<img>标签。因为加载图像不存在跨域问题。动态创建图像经常用于图像Ping。图像Ping是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204响应。eg:var img = new Image();img.onload = img.onerror = function(){ alert(“Done!”);}img.src = “http://www.example.com/test?name=Peter“; 图像Ping最常用于跟踪用户点击页面或动态广告曝光次数。图像Ping有两个缺点:一是只能发送GET请求;二是无法访问服务器的响应文本。 2.JSONP(JSON with padding 填充式JSON或参数式JSON)JSONP由两部分组成:回调函数和数据。通过查询字符串来指定JSONP服务的回调函数是很常见的。 JSONP是通过动态<script>元素来使用,使用时可以为src属性指定一个跨域URL。123456function handleResponse(response){ alert("You're at IP address "+response.ip + ",which is in " + response.city +", "+response.region_name);}var script = document.createElement("script");script.src = "http://freegeoip.net/json/?callback=handleResponse";document.body.insertBefore("script,document.body.firstChild"); JSONP的优点是:能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不足:JSONP是从其他域中加载代码执行,如果其他域不安全,很可能会在响应中夹带一些恶意代码;其次要确定JSONP请求是否失败并不容易。 CometComet指的是一种更高级的Ajax技术(经常也有人称为“服务器推送”)。Ajax是一种从页面向服务器请求数据的技术,而Comet则是一种服务器向页面推送数据的技术。 Comet能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。有两种实现Comet的方式:长轮询和流。长轮询是传统轮询(短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。而长轮询是把短轮询颠倒了。页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。 无论短轮询还是长轮询,浏览器都要在接收数据之前,先发起对服务器的连接。两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。以请客吃饭来讲述短轮询和长轮询:短轮询就是,我(server)说要请客吃饭,然后你(browser)就不停地发短信问我“今天请客吃饭不?”,你的行为无疑是很浪费钱的(耗资源);长轮询就是,我(server)说要请客吃饭,然后你(browser)就发条短信给我“下次请客吃饭记得叫我”,我就记得这件事,下次请客吃饭就会发短信通知你“我今天请客吃饭”。 第二种流行的Comet实现是HTTP流。流不同于上述两种轮询,因为它在页面的整个生命周期内只使用一个HTTP连接,即浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。 在Firefox、Chrome、Opera等浏览器中,通过侦听readyStatechange事件以及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。当readyState值变为3时,responseText属性中就会保存接收到的所有数据。使用XHR对象实现HTTP流的典型代码:123456789101112131415161718192021222324252627function createStreamingClient(url,progress,finished){ var xhr = new XMLHttpRequest(), received = 0; xhr.open("get",url,true); xhr.onreadystatechange = function(){ var result; if(xhr.readyState == 3){ //只取得最新数据并调整计数器 result = xhr.responseText.substring(received); received += result.length; //调用progress回调函数 progress(result); }else if(xhr.readyState == 4){ finished(xhr.responseText); } } xhr.send(null); return xhr;}var client = createStreamingClient("streaming.php",function(data){ alert("Received: "+data);},function(data){ alert("Done!");}); 这个createStreamingClient()函数接收三个参数:要连接的URL、在接收到数据时调用的函数以及关闭连接时调用的函数。 服务器发送事件(SSE)SSE是围绕只读Comet交互推出的API或者模式。SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。1.SSE API要预订新的事件流,首先要创建一个新的EventSource对象,并传进一个同源的入口点:1var source = new EventSource("myevents.php"); EventSource的实例有一个readyState属性,值为0表示正连接到服务器,值为1表示打开了连接,值为2表示关闭了连接。另外还有三个事件: open:在建立连接时触发。message:在从服务器接收到新事件时触发。error:在无法建立连接时触发。123source.onmessage = function(event){ var data = event.data;} 如果想强制立即断开连接并且不再重新连接,可以调用close()方法:source.close(); 2.事件流服务器事件会通过一个持久的HTTP响应发送,这个响应的MIME类型为text/event-stream.Web SocketsWeb Sockets的目标是在一个单独的持久连接上提供全双工、双向通信。 单工:如果在通信过程的任意时刻,信息只能从一方A传到一方B,则称为单工;半双工:如果在通信过程的任意时刻,信息既可以由A传到B,又能够从B传到A,但只能由一个方向上的传输存在,则称为半双工;全双工:如果在通信过程的任意时刻,线路上存在A到B和B到A的双向信号传输,则称为全双工。 过程:在Javascript中创建了WebSocket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为Web Socket协议。也就是说使用标准的HTTP服务器无法实现Web Sockets,只有支持这种协议的专门服务器才能正常工作。 Web Sockets使用了自定义的协议,有特定的URL模式:ws://,加密的连接是:wss://使用自定义协议而非HTTP协议的好处是:能够在客户端和服务器之间发送非常少量的数据,而不必担心HTTP那样字节级的开销。缺点:制定协议的时间比制定JavaScript API的时间还要长。 1.Web Sockets API1var socket = new WebSocket("ws://www.example.com/server.php"); 必须给WebSocket构造函数传入绝对URL,并且可以是跨域的URL。 与XHR类似,WebSocket也有readyState属性:WebSocket.OPENING(0):正在建立连接。WebSocket.OPEN(1):已经建立连接。WebSocket.CLOSING(2):正在关闭连接。WebSocket.CLOSE(3):已经关闭连接。 WebSocket没有readystatechange事件,readyState的值永远从0开始。 2.发送和接收数据12var socket = new WebSocket("ws://www.example.com/server.php");socket.send("Hello world!"); WebSocket只能通过连接发送纯文本信息,对于复杂的数据结构,如JSON数据,必须在连接发送之前进行序列化。123456var message = { time:new Date(), text: "Hello world!", clientId: "abcdef"};socket.send(JSON.stringify(message)); 当服务器向客户端发来消息时,WebSocket对象会触发message事件,返回的数据保存在event.data。 3.其他事件open:在成功建立连接时触发;error:在发生错误时触发,连续不能持续;close:在连接关闭时触发。 在这三个事件中,只有close事件的event对象有额外信息,分别是:wasClean、code和reason。 安全CSRF(跨站点请求伪造):未被授权系统有权访问某个资源的情况。为确保通过XHR访问的URL安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。有效做法:要求以SSL连接来访问可以通过XHR请求的资源;要求每一次请求都要附带经过算法计算得到的验证码; 无效做法(对于防范CSRF攻击不起作用):要求发送POST而不是GET请求——很容易改变;检查来源URL以确定是否可信——来源记录很容易伪造;基于cookie信息进行验证——很容易伪造。 不要将用户名和密码保存在Javascript代码中!!!]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(七)]]></title>
<url>%2F2017%2F05%2F16%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89%2F</url>
<content type="text"><![CDATA[HTML5脚本编程1.跨文档消息传递跨文档消息传送,有时简称XDM,指的是在来自不同域的页面间传递消息。XDM的核心是postMessage()方法,该方法的作用是:向另一个地方传递数据。对于XDM而言,“另一个地方”指的是包含在当前页面中的<iframe>元素,或者由当前页面弹出的窗口。postMessage()方法接收两个参数:一条消息和一个表示消息接收方来自哪个域的字符串。123//注意:所有支持XDM的浏览器也支持iframe的contentWindow属性var iframeWindow = document.getElementById("myframe").contentWindow;iframeWindow.postMessage("A secret","http://www.wrox.com"); 接收到XDM消息时,会触发window对象的message事件,是以异步形式触发的,因此从发送消息到接收消息(触发窗口的message事件)可能要经过一段时间的延迟。触发message事件后,传递给onmessage处理程序的事件对象(event)包含以下三方面的重要信息:data:作为postMessage()第一个参数传入的字符串数据。origin:发送消息的文档所在的域。source:发送消息的文档的window对象的代理。这个代理对象主要用于在发送上一条消息的窗口中调用postMessage()方法。如果发送消息的窗口来自同一个域,那这个对象就是window。123456789EventUtil.addHandler(window,"message",function(event){ //确保发送消息的域是已知的域 if(event.origin=="http://www.wrox.com"){ //处理接收到的数据 processsMessage(event.data); //可选:向来源窗口发送回执 event.source.postMessage("Received","http://p2p.wrox.com"); }}); event.source大多数情况下只是window对象的代理,不能通过这个代理对象访问window对象的其他任何信息。在使用postMessage()时,最好是只传字符串,如果想传入结构化的数据,最佳选择是先在要传入的数据上调用JSON.stringify(),通过postMessage()传入得到字符串,然后再在onmessage事件处理程序中调用JSON.parse()。 2.原生拖放拖放事件拖动某元素时,将依次触发下列事件:1)dragstart2)drag3)dragend当某个元素被拖动到一个有效的放置目标上时,下列事件会依次发生:1)dragenter2)dragover3)dragleave(拖出了目标)或drop(放到目标中) 自定义放置目标有些元素默认是不允许放置,但可以重写该元素的dragover和dragenter事件的默认行为,来使得元素变成有效的放置目标。1234567var droptarget = document.getElementById("droptarget");EventUtil.addHandler(droptarget,"dragover",function(event){ EventUtil.preventDefault(event);});EventUtil.addHandler(droptarget,"dragenter",function(event){ EventUtil.preventDefault(event);}); 在firefox3.5+中,放置事件的默认行为是打开被放到放置目标上的URL,因此,为了让firefox 支持正常的拖放,还要取消事件的默认行为:EventUtil.addHandler(droptarget,”drop”,function(event){ EventUtil.preventDefault(event);}); dataTransfer对象为了在拖放操作时实现数据交换,引入dataTransfer 对象,它是事件对象的一个属性,用于从被拖动元素向放置目标传递字符串格式的数据。dataTransfer对象主要有两个方法:getData()和setData()。123456//设置和接收文本数据event.dataTransfer.setData("text","some text");var text = event.dataTransfer.getData("text");//设置和接收URLevent.dataTransfer.setData("URL","http://www.wrox.com/");var text = event.dataTransfer.getData("URL"); 为了实现跨浏览器从dataTransfer对象取得数据,最好在取得URL数据时检测两个值,而在取得文本数据时使用“Text”.12345var dataTransfer = event.dataTransfer;//读取URLvar url = dataTransfer.getData("url") ||dataTransfer.getData("text/uri-list");//读取文本var text = dataTransfer.getData("Text") ; dropEffect与effectAlloweddataTransfer对象的两个属性:dropEffect和effectAllowed.dropEffect有4个可能d值:none:不能把拖动的元素放在这里。move:应该把拖动的元素移动到放置目标。copy:应该把拖动的元素复制到 放置目标。link:表示放置目标会打开拖动的元素(但拖动的元素必须是一个链接,有URL)。 dropEffect属性只有搭配effectAllowed属性才有用。effectAllowed有以下可能的值:uninitialized:没有给被拖动的元素设置任何放置行为。none:被拖动元素不能有任何行为。copy:只允许值为“copy”的dropEffect。link:只允许值为“link”的dropEffect。move:只允许值为“move”的dropEffect。copyLink:允许值为“copy”和“link”的dropEffect。copyMove:允许值为“copy”和“move”的dropEffect。linkMove:允许值为“link”和“move”的dropEffect。all:允许任意dropEffect。 必须在ondragstart事件处理程序中设置effectAllowed属性。 可拖动默认情况下,图像、链接和文本是可以拖动的,但文本只有在被选中的情况下才可被拖动。要想让其他元素也可被拖动,HTML5为所有HTML元素规定了draggable属性,表示元素是否可以拖动。1<div draggable="true">...</div> 其他成员HTML5还规定了dataTransfer对象还应该包含下列方法和属性:addElement(element):为拖动操作添加一个元素。clearData(format):清除以特定格式保存的数据。setDragImage(element,x,y):指定一副图像,当拖动发生时,显示在光标下方。这个方法接收三个参数分别是要显示的HTML元素和光标在图像中的x、y坐标。types:当前保存的数据类型。 拖拽实现原理计算出鼠标移动的距离,然后将元素的坐标位置加上鼠标前后坐标之差,最后改变元素的top和left值1234nn6 = document.getElementById && !document.all;//判断是否是IE6浏览器dragObj.style.top = (nn6 ? objY + e.clientY - mouseY : objY + event.clientY - mouseY) + "px";dragObj.style.left = (nn6 ? objX + e.clientX - mouseX : objX + event.clientX - mouseX) + "px";(e.clientX和e.clientY是鼠标当前的坐标位置,mouseX和mouseY是鼠标原来的坐标位置,objX和objY是元素的坐标位置) 媒体元素<video>和<audio>常用属性:全部属性:常用方法事件 自定义媒体播放器1234567891011<div class="mediaplayer"> <div class="video"> <video id="player" src="movie.mov" poster="mymovie.jpg" width="300" height="200"> Video player not available. </video> </div> <div class="controls"> <input type="button" value="Play" id="video-btn"/> <span id="curtime">0</span><span id="duration">0</span> </div></div> 123456789101112131415161718192021var player = document.getElementById("player"), btn = document.getElementById("video-btn"), curtime = document.getElementById("curtime"), duration = document.getElementById("duration");//更新播放时间duration.innerHTML = player.duration;//为按钮添加事件处理程序EventUtil.addHandler(btn,"click",function(event){ if (player.paused) { player.play(); btn.value = "Pause"; }else{ player.pause(); btn.value = "Play"; }});//定时更新当前时间setInterval(function(){ curtime.innerHTML = player.currentTime;},250); 检测编码器(浏览器)的支持情况canPlayType()方法,该方法接收一种格式/编解码器字符串,返回“probably”、“maybe”和“”(空字符串)。空字符串是假值,其他两个位真值,会转换为true。123if(audio.canPlayType("audio/mpeg")){ } Audio类型<audio>元素还有一个原生的Javascript构造函数Audio,与Images很相似。1var audio = new Audio("sound.mp3"); 历史状态管理通过hashchange事件,可知道URL的参数什么时候发生了变化。通过状态管理API(HTML5的history对象),能够在不加载新页面的情况下改变浏览器的URL。history.pushState()方法接收三个参数:状态对象、新状态的标题和可选的相对URL。1history.pushState({name:"Peter"},"Peter' page","peter.html"); 执行pushState()方法后,新的状态信息就会被加入到历史状态栈中。当按下“后退”按钮时,会触发window对象的popstate事件。popstate事件的事件对象有一个state属性。当单击“后退”按钮返回浏览器加载的第一个页面时,event.state=null。更新当前状态,调用replaceState(),传入参数与pushState()的前两个参数相同。传递给pushState()或replaceState()的状态对象不能包含DOM元素。 重点了解利用history.pushState实现的pjax技术。 JSONJSON是一种数据格式,不是一种编程语言,且并不从属与Javascript。1.语法JSON语法可以表示以下三种类型的值:简单值:使用与javascript相同的语法,可以在JSON中表示字符串、数值、布尔值和null,但不支持javascript中的undefined。JSON字符串必须使用双引号(单引号会导致语法错误) 对象:对象作为一种复杂数据类型,表示的是一组无序的键值对。而每个键值对中的值可以是简单值,也可以是复杂数据类型的值。JSON中的对象要求给属性加双引号,此外JSON没有变量的概念。1234{ "name":"Peter", "age":29} 数组:表示一组有序的值的列表,可以通过数值索引来访问其中的值。数组的值可以是任意类型——简单值、对象或数组。eg:[25,”hi”,true] 2.解析与序列化JSON对象早期的JSON解析器基本上是使用javascript的eval()函数,但存在风险。JSON对象有两个方法:stringify()和parse()。在最简单的情况下,这两个方法分别用于把javascript对象序列化为JSON字符串和把JSON字符串解析为原生javascript值。12345678910111213141516var book={ title:"Professional JavaScript", authors:["Nicholas C. Zakas"], edition: 3, year:2011};var jsonText = JSON.stringify(book); //将javascript对象序列化为JSON字符串在jsonText中:{ "title":"Professional JavaScript", "authors":["Nicholas C. Zakas"], "edition": 3, "year":2011}var boookCopy = JSON.parse(jsonText);//将JSON字符串解析为原生javascript值 序列化选项JSON.stringify()除了要序列化的javascript对象外,还可以接收另外两个参数,这两个参数用于指定以不同的方式序列化javascript对象。第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在JSON字符串中保留缩进。1.过滤结果如果过滤参数是数组,那么JSON.stringify()的结果中将只包含数组中列出的属性。1234567var book={ "title":"Professional JavaScript", "authors":["Nicholas C. Zakas"], edition: 3, year:2011};var jsonText = JSON.stringify(book,["title","edition"]) 在jsonText中为:1234{ "title":"Professional JavaScript", "edition": 3} 如果第二个参数是函数,行为会稍有不同。传入的函数接收两个参数,属性(键)名和属性值。根据属性(键)名可以知道应该如何处理要序列化的对象中的属性。属性名只能是字符串,而在值并非键值对结构的值时,键名可以是空字符串。函数返回的值就是相应键的值,但函数返回的是undefined,那么相应属性会被忽略。123456789101112131415var book={ "title":"Professional JavaScript", "authors":["Nicholas C. Zakas"], edition: 3, year:2011};var jsonText = JSON.stringify(book,function(key,value){ switch(key){ case "authors": return value.join(","); case "year": return 5000; case "edition": return undefined; default: return value; }}); 序列化后,jsonText值为:12345{ "title":"Professional JavaScript", "authors":["Nicholas C. Zakas"], "year":5000} 2.字符串缩进JSON.stringify()方法的第三个参数数用于控制结果中的缩进和空白符。第三个参数为数值时,表示每个级别缩进的空格数,最大缩进格数为10;如果为字符串非数值,则这个字符串将在JSON字符串中被用作缩进字符(不再使用空格) 3.toJSON()方法1234567891011var book={ "title":"Professional JavaScript", "authors":["Nicholas C. Zakas"], edition: 3, year:2011, toJSON:function(){ return this.title; }};var jsonText = JSON.stringify(book);//图书的书名 序列化对象的顺序1)如果存在toJSON()而且能通过它取得有效的值,则先调用该方法,否则返回对象本身。2)如果提供了第二个参数,应用这个函数过滤器。3)对第二步返回的每个值进行相应的序列化。4)如果提供了第三个参数,执行相应的格式化 解析选项JSON.parse()也可以像接收另一个参数,该参数是一个函数(还原函数),将在每一个键值对上调用。如果还原函数返回undefined,则表示要从结果中删除相应的键;如果返回其他值,则将该值插入到结果中。12345678910111213141516var book={ "title":"Professional JavaScript", "authors":["Nicholas C. Zakas"], edition: 3, year:2011, releaseDate: new Date(2015,11,8);};var jsonText = JSON.stringify(book);var bookCopy = JSON.parse(jsonText,function(key,value){ if(key=="releaseDate"){ return new Date(value); }else{ return value; }}); 结果为:bookCopy.releaseDate属性中保存一个Date对象。]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(六)]]></title>
<url>%2F2017%2F05%2F15%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AD%EF%BC%89%2F</url>
<content type="text"><![CDATA[使用Canvas绘图基本用法要使用<canvas>元素,必须先设置其width和height,指定可以绘图的区域大小。要在这块画布(canvas)上绘图吗,需要取得绘图上下文,是通过getContext(),然后传入上下文的名字。传入“2d”,就可以取得2D上下文对象。1<canvas id="drawing" width="200" height="200">您的浏览器不支持canvas元素</canvas> 12345var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if(drawing.getContext){ var context = drawing.getContext("2d");} 使用toDataURL()方法,可以导出在<canvas>元素上绘制的图像。这个方法接受一个参数,即图像的MIME类型格式,而且适用于创建图像的任何上下文。12345678910var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if(drawing.getContext){ //取得图像的数据URI var imgURI = drawing.toDataURL("image/png"); //显示图像 var image = document.createElement("img"); image.src = imgURI; document.body.appendChild(image);} 2D上下文1.填充和描边填充,就是用指定的样式(颜色、渐变、或图像)填充图形,取决于fillStyle属性;描边,就是只在图形的边缘画线,取决于strokeStyle属性。fillStyle和strokeStyle属性的值可以是字符串、渐变对象或模式对象,它们的默认值都是“#000000”。123456var drawing = document.getElementById("drawing");if(drawing.getContext){ var context = drawing.getContext("2d"); context.strokeStyle = "red"; context.fiilStyle = "#0000ff";} 2.绘制矩形矩形是唯一一种可以直接在2D上下午中绘制的形状。与矩形有关的方法包括:fillRect()、strokeRect()和clearRect()。这三个方法都接收4个参数:矩形的x坐标、矩形的y坐标、矩形宽度和矩形高度。这些参数的单位都是像素。首先,fillRect()方法会在画布上绘制的矩形会填充指定的颜色。填充的颜色通过fillStyle属性指定。1234567891011var drawing = document.getElementById("drawing");if(drawing.getContext){ var context = drawing.getContext("2d"); //绘制红色矩形 context.fillStyle = "#ff0000"; context.fillRect(10,10,50,50); //绘制半透明的蓝色矩形 context.fillStyle = "rgba(0,0,255,0.5)"; context.fillRect(30,30,50,50);} strokeRect()方法在画布上绘制的是矩形边框,边框颜色由strokeStyle属性指定。描边线条的跨度由lineWidth属性控制,该属性的值可以是任意整数。另外,通过lineCap属性可以控制线条末端的形状是平头、圆头还是方头(“butt”,“round”,“square”),通过lineJoin属性可以控制线条相交的方式是圆交、斜交还是斜接(“round”,“bevel”,“miter”); clearRect()方法用于清除画布上的矩形区域,区域大小由该方法中传入的参数决定。本质上,这个方法可以绘制上下文中的某一矩形区域变透明。12345678910111213var drawing = document.getElementById("drawing");if(drawing.getContext){ var context = drawing.getContext("2d"); // 绘制红色矩形 context.fillStyle = "#ff0000"; context.fillRect(10, 10, 50, 50); // 绘制半透明的蓝色矩形 context.fillStyle = "rgba(0, 0, 255, 0.5)"; context.fillRect(30, 30, 50, 50); // 在两个矩形重叠的地方清除一个小矩形 context.clearRect(40, 40, 10, 10);} 3.绘制路径要绘制路径,首先必须调用beginPath()方法,表示要开始绘制新路径。然后通过下列方法来实际地绘制路径:arc(x, y, radius, startAngle, endAngle, counterclockwise):以(x,y)为圆心绘制一条弧线,弧线半径为radius,起始和结束角度(用弧度表示)分别为startAngle和endAngle。最后一个参数表示startAngle和endAngle是否按逆时针计算,值为false表示按顺时针方向计算。arcTo(x1, y1, x2, y2, radius):从上一点开始绘制一条弧线,到(x2,y2)为止,并且以给定的半径radius穿过(x1,y1)。bezierCurveTo(c1x, c1y, c2x, c2y, x, y):从上一点开始绘制一条曲线,到(x,y)为止,并且以(c1x,c1y)和(c2x,c2y)为控制点。lineTo(x, y):从上一点开始绘制一条直线,到(x,y)为止。moveTo(x, y):将绘图游标移动到(x,y),不画线。quadraticCurveTo(cx, cy, x, y):从上一点开始绘制一条二次曲线,到(x,y)为止,并且以(cx,cy)作为控制点。rect(x, y, width, height):从点(x,y)开始绘制一个矩形,宽度和高度分别由width和height指定。这个方法绘制的是矩形路径,而不是strokeRect()和fillRect()所绘制的独立的形状。 如果想绘制一条连接到路径起点的线条,可以调用closePath().如果你想用fillStyle填充它,可以调用fill()方法。另外,还可以调用stroke()方法对路径描边,描边使用的是strokeStyle。还可以调用clip()在路径上创建一个剪切区域。1234567891011121314151617181920212223242526//绘制一个不带数字的时钟表盘var drawing = document.getElementById("drawing");if(drawing.getContext){ var context = drawing.getContext("2d"); //开始绘制路径 context.beginPath(); //绘制外圆 context.arc(100,100,99,0,2*Math.PI,false); //绘制内圆 context.moveTo(194,100); context.arc(100,100,94,0,2*Math.PI,false); //绘制分针 context.moveTo(100,100); context.lineTo(100,15); //绘制时针 context.moveTo(100,100); context.lineTo(35,100); //描边路径 context.stroke();} 在路径使用频繁时,使用isPointInPath()方法判断路径被关闭之前画布某一点是否位于路径上,接收两个参数:x坐标和y坐标。 绘制文本绘制文本主要有两个方法:fillText()和strokeText()。这两个方法都可以接收4个参数:要绘制的文本字符串、x坐标、y坐标和可选的最大像素宽度。这两个方法都以下列3个属性为基础:font:表示文本样式、大小及字体,用CSS中指定字体的格式来指定。textAlign:表示文本对齐方式。可能的值有“start”、“end”、“left”、“right”和“center”,建议使用“start”和“end”,更稳定。textBaseline:表示文本的基线。可能的值有”top“、”hanging“、”middle“、”alphabetic“、”ideographic“和”bottom“。1234context.font = "bold 14px Arial";context.textAlign = "center";context.textBaseline = "middle";context.fillText("12",100,20); 2D上下文提供了辅助确定文本大小的方法measureText()。这个方法接收一个参数,即要绘制的文本;返回一个TextMetrics对象。返回的对象目前只有一个width属性。measureText()方法利用font、textAlign和textBaseline的当前值计算指定文本的大小。12345678var fontSize = 100;context.font = fontSize +"px Arial";while(context.measureText("Hello world!").width > 140){ fontSize--; context.font = fontSize + "px Arial";}context.fillText("Hello world!",10,10);context.fillText("Font size is " +fontSize +"px",10,50); 变换通过上下文的变换,可以把处理后的图像绘制到画布上。通过以下方法来修改变换矩阵:rotate(angle):围绕原点旋转图像angle弧度。 旋转角度,以弧度计。如需将角度转换为弧度,请使用 degreesMath.PI/180 公式进行计算。举例:如需旋转 5 度,可规定下面的公式:5Math.PI/180。context.rotate(5*Math.PI/180); scale(scaleX,scaleY):缩放图像,在x方向乘以scaleX,在y方向乘以scaleY。scaleX和scaleY的默认值都是1.0。translate(x,y):都是将坐标原点移动到(x,y)。执行这个变换之后,坐标(0,0)会变成之前由(x,y)表示的点。transform(m1_1, m1_2, m2_1, m2_2, dx, dy):直接修改变换矩阵,方式是乘以如下矩阵。 m1_1 m1_2 dx m2_1 m2_2 dy 0 0 1 setTransform(m1_1, m1_2, m2_1, m2_2, dx, dy):将变换矩阵重置为默认状态,然后再调用transform()。还由两个方法可以跟踪上下文的状态变化,如果你知道将来还要返回某组属性与变换的组合,可以调用save()方法;等想要回到之前保存的设置时,可以调用restore()方法,在保存设置的栈结构中向前返回一级,恢复之前的状态。连续调用save()可以把更多设置保存到栈结构中,之后再连续调用restore()则可以一级一级返回。123456789101112131415context.fillStyle = "#ff0000";context.save();context.fillStyle = "#00ff00";context.translate(100,100);context.save();context.fillStyle = "#0000ff";context.fillRect(0,0,100,200); //从点(100,100)开始绘制蓝色矩形context.restore();context.fillRect(10,10,100,200); //从点(110,110)开始绘制绿色矩形context.restore();context.fillRect(0,0,100,200); //从点(0,0)开始绘制红色矩形 需要注意的是,save()方法保存的只是对绘图上下文的设置和变换,不会保存绘图上下文的内容。 绘制图像2D上下文内置了对图像的支持,通过drawImage()方法可以将一副图像绘制到画布上。该方法有三种参数组合:12345678var image = document.images[0];//传入HTML<img>元素,以及绘制该图像的起点的x和y坐标。context.drawImage(image, 10, 10);//要想改变图像大小,可以再多传入两个参数:目标宽度和目标高度context.drawImage(image, 50, 10, 20, 30);//把图像中的某个区域绘制到上下文中,传入9个参数:要绘制的图像、源图像的x坐标、源图像的y坐标、源图像的宽度、//源图像的高度、目标图像的x坐标、目标图像的y坐标、目标图像的宽度、目标图像的高度。context.drawImage(image, 0, 10, 50, 50, 0, 100, 40, 60); 阴影2D上下文会根据以下几个属性的值,自动为形状或路径绘制出阴影:shadowColor:用CSS颜色格式表示的阴影颜色,默认为黑色。shadowOffsetX:形状或路径x轴方向的阴影偏移量,默认为0。shadowOffsetY:形状或路径y轴方向的阴影偏移量,默认为0。shadowBlur:模糊的像素数,默认为0,即不模糊。12345678910111213var drawing = document.getElementById("drawing");var context = drawing.getContext("2d");//设置阴影context.shadowOffsetX = 5;context.shadowOffsetY = 5;context.shadowBlur = 4;context.shadowColor = "rgba(0,0,0,0.5)";//绘制红色矩形context.fillStyle = "#ff0000";context.fillRect(10, 10, 50, 50);//绘制蓝色矩形context.fillStyle = "rgba(0,0,255,1)";context.fillRect(30, 30, 50, 50); 渐变渐变由CanvasGradient实例表示,能够通过2D上下文来创建和修改。要创建一个新的线性渐变,可以调用createLinearGradient()方法。这个方法接收4个参数:起点的x坐标、起点的y坐标、终点的x坐标、终点的y坐标。调用这个方法后,它就会创建一个指定大小的渐变,并返回CanvasGradient对象的实例。创建了渐变对象后,下一步就是使用addColorStop()方法来指定色标。这个方法接收两个参数:色标位置和CSS颜色值。色标位置是一个0(开始的颜色)到1(结束的颜色)之间的数字。123456var gradient = context.createLinearGradient(30,30,70,70);gradient.addColorStop(0,"white");gradient.addColorStop(1,"black");//将fillStyle或strokeStyle设置为这个对象,从而使用渐变来绘制形状或描边context.fillStyle = gradient ;context.fillRect(30,30,50,50); 为了确保渐变与形状对齐,有时候可以考虑使用函数来确保坐标合适:123function createRectLinearGradient(context,x,y,width,height){ return context.createLinearGradient(x, y, x+width, y+height);} 创建径向渐变(或放射渐变),可以使用createRadialGradient()方法。这个方法接收6个参数,对应着两个圆的圆心和半径。前三个是起点圆的圆心(x和y)及半径,后三个是终点圆的圆心(x和y)及半径。如果想从某个形状的中心点开始创建一个向外扩散的径向渐变效果,就要将两个圆定义为同心圆。123456var gradient = context.createRadialGradient(55,55,10,55,55,30);gradient.addColorStop(0,"white");gradient.addColorStop(1,"black");context.fillStyle = gradient ;context.fillRect(30,30,50,50); 模式模式其实就是重复的图像,可以用来填充或描边图形。通过createPattern()方法来创建新模式,该方法接收两个参数:一个HTML<img>元素和一个表示如何重复图像的字符串。第二个参数值与CSS的background-repeat属性值相同:repeat、repeat-x、repeat-y、no-repeat。123456789101112131415var image = document.image[0];pattern = context.createPattern(image,"repeat");//绘制矩形context.fillStyle = pattern;context.fillRect(10,10,150,150);需要注意的是模式对象的生成和赋值过程必须写到JavaScript的function中,否则可能会失效。window.onload = function() { var drawing = document.getElementById("drawing"); var context = drawing.getContext("2d"); var img = new Image(); img.src = "http://imgsrc.baidu.com/forum/pic/item/e6b14bc2a4561b1fe4dd3b24.jpg"; var pattern = context.createPattern(img, 'repeat'); context.fillStyle = pattern; context.fillRect(0, 0, canvas.width, canvas.height);} 使用图像数据通过getImageData()取得原始图像数据。这个方法接收四个参数:要取得其数据的画面区域的x和y坐标以及该区域的像素宽度和高度。返回ImageData对象实例,每个ImageData对象都有三个属性:width、height和data。data属性是一个数组,保存着图像中每一个像素的数据。在data数组中,每一个像素用4个元素来保存,分别表示红、绿、蓝和透明度值。123456var imageData = context.getImageData(10,5,50,50);var data = imageData.data;red = data[0];green = data[1];blue = data[2];alpha = data[3]; 通过修改图像数据,可以像下面这样创建一个简单的灰阶过滤器:123456789101112131415161718192021222324252627/*灰阶过滤器*/var drawing = document.getElementById("drawing");if(drawing.getContext){ var context = drawing.getContext("2d"); var image = document.images[0]; var imageData, data, i, len, average, red, green, blue, alpha; //绘制原始图像 context.drawImage(image,0,0); //取得图像数据 imageData = context.getImageData(0,0,image.width,image.height); data = imageData.data; for (i = 0,len = data.length; i < len; i += 4) { red = data[i]; green = data[i+1]; blue = data[i+2]; alpha = data[i+3]; //求得rgb平均值 average = Math.floor((red+green+blue)/3); //设置颜色值,透明度不变 data[i] = average; data[i+1] = average; data[i+2] = average; } imageData.data = data; //将图像数据绘制到画布上 context.putImageData(imageData,0,0);} 需要注意的是,在本地测试时会报错,因为本地测试用的图片是文件夹内的,js跨域限制是不能获取非同一域名下的数据的,而本地的位置是没有域名的,所以浏览器都认为你是跨域,导致报错。 合成还有两个会应用到2D上下文中所有绘制操作的属性:globalAlpha和globalCompositionOperation.globalAlpha是一个介于0和1之间的值(包括0和1),用于指定所有绘制的透明度。默认值为0;如果所有后续操作都要基于相同的透明度,就可以先把globalAlpha设置为适当的值,然后绘制,最后再设置为0。12345678910//绘制红色矩形context.fillStyle = "#ff0000";context.fillRect(10,10,50,50);//修改全局透明度context.globalAlpha =0.5;//绘制蓝色矩形context.fillStyle="rgba(0,0,255,1)";context.fillRect(30,30,50,50);//重置全局透明度context.globalAlpha = 0; globalCompositionOperation表示后绘制的图形怎样与先绘制的图形结合。有以下属性值(字符串):source-over(默认值):后绘制的图形位于先绘制的图形上方。source-in:后绘制的图形与先绘制的图形重叠部分可见,两者其他部分完全透明。source-out:后绘制的图形与先绘制的图形重叠部分不可见,先绘制的图形完全透明。source-atop:后绘制的图形与先绘制的图形重叠部分可见,先绘制的图形不受影响。destination-over:后绘制的图形位于先绘制的图形下方,只有之前透明像素下的部分才可见。destination-in:后绘制的图形位于先绘制的图形下方,两者不重叠的部分完全透明。destination-out:后绘制的图形擦除与先绘制的图形重叠的部分。destination-atop:后绘制的图形位于先绘制的图形下方,在两者不重叠的地方,先绘制的图形变透明。lighter:后绘制的图形与先绘制的图形重叠部分的值相加,使该部分变亮。copy:后绘制的图形完全替代与之重叠的先绘制图形。xor:后绘制的图形与先绘制的图形重叠的部分执行“异或”操作。 WebGLWebGL是针对Canvas的3D上下文。类型化数组WebGL涉及的复杂计算需要提前知道数值的精度,而标准的Javascript数值无法满足需求。为此,WebGL引入了类型化数组。类型化数组也是数组,只不过其元素被设置为特定类型的值。类型化数组的核心是一个名为ArrayBuffer(数组缓冲器类型)的类型。每个ArrayBuffer对象表示的只是内存中指定的字节数,但不会指定这些字节用于保存什么类型的数据。通过ArrayBuffer能够为将来使用分配一定数量的字节。12var buffer = new ArrayBuffer(20);//在内存中分配20字节var bytes = buffer.byteLength; //通过buffer对象获取唯一的信息,字节数 1.视图使用ArrayBuffer的一种特别的方式就是用它来创建数组缓冲器视图。最常见的视图是DataView,通过它可以选择ArrayBuffer中一小段字节。123456//基于整个缓冲器创建一个新视图var view = new DataView(buffer);//创建一个开始于字节9的新视图var view = new DataView(buffer, 9);//创建一个从字节9开始到字节18的新视图var view = new DataView(buffer, 9, 10); DataView对象会把字节偏移量以及字节长度信息分别保存在byteOffset和byteLength属性中。 2.类型化视图类型化视图一般也被称为类型化数组。有以下几种:Int8Array、Uint8Array、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array. 如果为相应元素指定的字节数放不下相应的值,则实际保存的值是最大可能值的模。类型化视图还有一个方法,subarray(),使用这个方法可以基于底层数组缓冲器的子集创建一个新视图。这个方法接收两个参数:开始元素的索引和可选的结束元素的索引。返回的类型与调用该方法的视图类型相同。 WebGL上下文在使用WebGL上下文时,务必先检测一下返回值。1<canvas id="drawing" width="200" height="200">您的浏览器不支持canvas元素</canvas> 1234567var drawing = document.getElementById("drawing");if(drawing.getContext){ var gl = drawing.getContext("experimental-webgl"); if(gl){ //使用WebGL }} 一般都把WebGL上下文对象命名为gl。通过给getContext()传递第二个参数,可以为WebGL上下文设置一些选项。这个参数本身是一个对象,有以下属性:alpha:值为true,表示为上下文创建一个Alpha通道缓冲区;默认为true。depth:值为true,表示可以使用16位深缓冲区;默认值为true。stencil:值为true,表示可以使用8位模板缓冲区;默认值为false。antialias:值为true,表示将使用默认机制执行抗锯齿操作;默认为true。premultipliedAlpha:值为true,表示绘图缓冲区有预乘Alpha值;默认值为true。preserveDrawingBuffer:值为true,表示在绘图完成后保留绘图缓冲区;默认值为false。1234567var drawing = document.getElementById("drawing");if(drawing.getContext){ var gl = drawing.getContext("experimental-webgl",{alpha:false}); if(gl){ //使用WebGL }} 如果getContext()无法创建WebGL上下文,有的浏览器会报错,为此,最好把调用封装到try-catch块中。1234567891011121314var drawing = document.getElementById("drawing"), gl;if(drawing.getContext){ try{ gl = drawing.getContext("experimental-webgl"); }catch(ex){ } if(gl){ //使用WebGL }else{ alert("WebGL context could not be created."); }} 1.常量eg:gl.COLOR_BUFFER_BIT 2.方法命名方法名的后缀包含参数个数(1到4)和接收的数据类型(f表示浮点数,i表示整数),而v代表接收数组eg:gl.uniform3i()接收3个整数。gl.uniform4f()接收4个浮点数。gl.uniform3iv()接收一个包含3个值的整数数组。 3.准备绘图在实际操作WebGL上下文之前,一般都要使用某种实色清除<canvas>,为绘图做好准备。首先必须使用clearColor()方法来指定要使用的颜色值,该方法接收4个参数:红、绿、蓝和透明度。每个参数都必须是一个0到1之间的数值,表示每种分量在最终颜色中的强度。eg:gl.clearColor(0,0,0,1);gl.clear(gl.COLOR_BUFFER_BIT); 4.视口与坐标开始绘图之前,通常需要先定义WebGL视口(viewport)。默认情况下,视口可以使用整个<canvas>区域。要改变视口的大小,可以调用viewport()方法并传入4个参数:(视口相对于<canvas>元素的)x坐标、y坐标、宽度和高度。12//调用<canvas>元素gl.viewport(0,0,drawing.width,drawing.height); 视口坐标的原点在<canvas>元素的左下角,x轴和y轴的正方向分别是向右和向上。 另外,在视口内部的坐标系与定义视口的坐标系也不一样,在视口内部,坐标原点(0,0)是视口的中心点。在视口内部绘图时使用视口外部坐标,结果可能会被视口剪切。 5.缓冲区顶点信息保存在javascript的类型化数组中,使用之前必须转换到WebGL缓冲区。12345678var buffer =gl.createBuffer(); //创建缓冲区gl.bindBuffer(gl.ARRAY_BUFFER,buffer); //绑定到WebGL上下文gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([0,0.5,1]),gl.STATIC_DRAW);gl.bufferData()的最后一个参数用于指定使用缓冲区的方式:gl.STATIC_DRAW:数据只加载一次,在多次绘图中使用。gl.STREAM_DRAW:数据只加载一次,在几次绘图中使用。gl.DYNAMIC_DRAW:数据动态改变,在多次绘图中使用。 使用gl.deleteBuffer()释放内存。 6.错误WebGL操作一般不会抛出错误,为了知道是否有错误发生,必须在调用某个可能出错的方法后,手工调用gl.getError()。 7.着色器WebGL中有两种着色器:顶点着色器和片段(或像素)着色器。顶点着色器用于将3D顶点转换为需要渲染的2D点。片段着色器用于准确计算要绘制的每个像素的颜色。着色器是用GLSL语言编写的。 8.编写着色器略9.编写着色器程序略10.为着色器传入值略11.调试着色器和程序略 12.绘图WebGL只能绘制三种形状:点、线和三角。执行绘图操作要调用gl.drawArrays()或gl.drawElements()方法,前者用于数组缓冲区,后者用于元素数组缓冲区。gl.drawArrays()或gl.drawElements()的第一个参数都是一个常量,表示要绘制的形状,常量取值如下:gl.POINTS:将每个顶点当成一个点来绘制。gl.LINES:将数组当成一系列顶点,在这些顶点间画线。每个顶点既是起点也是终点,因此数组中必须包含偶数个顶点。gl.LINE_LOOP:将数组当成一系列顶点,在这些顶点间画线。最后一个顶点画到第一个顶点,形成一个形状的轮廓。gl.LINE_STRIP:除了不花最后一个顶点与第一个顶点之间的线之外,其他与gl.LINE_LOOP相同。gl.TRIANGLES:将数组当成一系列顶点,在这些顶点间画三角形。gl.TRIANGLES_STRIP:除了前三个顶点之后的顶点当作第三个顶点与前两个顶点共同构成一个新三角形外,其他与gl.TRIANGLES相同。gl.TRIANGLES_FAN:除了前三个顶点之后的顶点当作第三个顶点与前一个顶点及第一个顶点共同构成一个新三角形外,其他与gl.TRIANGLES相同。例如,数组中有A、B、C、D四个顶点,则第一个是三角形连接ABC,第二个三角形连接BCD。12345678910111213141516171819//假设已经使用了着色器请处理视口//定义三个顶点以及每个顶点的x 和y 坐标var vertices = new Float32Array([0,1,1,-1,-1,-1]), buffer = gl.createBuffer(), vertexSetSize = 2, vertexSetCount = vertices.length/vertexSetSize, uColor,aVertexPosition;//把数据放到缓冲区gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);//为片段着色器传入颜色值uColor = gl.getUniformLocation(program,"uColor");gl.uniform4fv(uColor,[0,0,0,1]);//为着色器传入顶点信息aVertexPosition = gl.getAttribLocation(program,"aVertextPosition");gl.enableVertexAttribArray(aVertexPosition);gl.vertexAttribPointer(aVertexPosition,vertexSetSize,gl.FLOAT,false,0,0);//绘制三角形gl.drawArrays(gl.TRIANGLES,0,vertexSetCount); 13.纹理WebGL的纹理可以使用DOM中的图像。调用gl.createTexture()创建一个新纹理。12345678910111213var image = new Image(),texture;image.src="simple.gif";iimage.onload = function(){ texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D,texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,true); gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED,image); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST); gl.textParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST); //清除当前纹理 gl.bindTexture(gl.TEXTURE_2D,null);}; 14.读取像素通过gl.readPixels()方法读取像素。]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(五)]]></title>
<url>%2F2017%2F05%2F14%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%94%EF%BC%89%2F</url>
<content type="text"><![CDATA[事件Javascript与HTML之间的交互是通过事件来实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。 事件流事件流描述的是从页面中接收事件的顺序。IE8及之前版本的事件流仅是事件冒泡流。 事件冒泡IE的事件流叫做事件冒泡,即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。打个比方说:你在地方法院要上诉一件案子,如果地方没有处理此类案件的法院,地方相关部门会帮你继续往上级法院上诉,比如从市级到省级,直至到中央法院,最终使你的案件得以处理。最常应用于事件委托技术中。 事件捕获事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于事件到达预定目标之前捕获它。 DOM事件流“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段,处于目标阶段和事件冒泡阶段。 事件处理程序事件是指用户或浏览器自身执行的某种动作,如click;事件处理程序是指响应某个事件的函数,也叫做事件侦听器,如onclick()。 事件处理程序有以下五类:1)HTML事件处理程序直接在HTML中调用javascript代码或函数;有点:简单,方便;缺点:javascript和html紧密耦合,不方便维护和修改。123456<script> function showMessage() { alert("Hello world!"); }</script><input type="button" value="click me" onclick="showMessage()"> 2)DOM0级事件处理程序通过javascript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。123456var btn = document.getElementById("myBtn");btn.onclick = function(){ alert(this.id);};btn.onclick = null; //删除事件处理程序 3)DOM2级事件处理程序“DOM2级事件”定义了两个方法,用于处理和删除事件处理程序的操作:addEventListener()和removeEventListener()所有DOM节点都包含这两个方法,它们都接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。1234var btn = document.getElementById("myBtn");btn.addEventListener("click",function(){ alert(this.id);},false); 使用DOM2级方法主要好处是可以添加多个事件处理程序。通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数将无法移除。 4)IE事件处理程序IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。1234var btn = document.getElementById("myBtn");btn.attachEvent("onclick",function(){ alert("Clicked");}); 在IE中使用attachEvent()与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用DOM0级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此this等于window。 使用attachEvent()添加的事件可以通过detachEvent()来移除,但必须提供相同的参数,同样地,无法将匿名函数移除。支持IE事件处理程序的浏览器有IE和Opera。 5)跨浏览器的事件处理程序自定义合适的事件处理的方法,使其能够在大多数浏览器下一致地运行。创建addHandler()方法,它的职责是视情况分别使用DOM0级方法、DOM2级方法或IE方法来添加事件。addHandler()方法接收3个参数:要操作的元素、事件名称和事件处理程序函数。123456789101112131415161718192021EventUtil={ addHandler: function(element,type,handler) { if(element.addEventListener){ element.addEventListener(type,handler,false); }else if(element.attachEvent){ element.attachEvent("on"+type,handler); }else{ element["on"+type] = handler; } }, removeHandler: function(element,type,handler) { if(element.removeEventListener) { element.removeEventListener(type,handler,false); }else if(element.detachEvent) { element.detachEvent("on"+type,handler); }else{ element["on"+type] = null; } }}; 事件对象在触发DOM上的某个事件时,会产生一个事件对象event。所有浏览器都支持event对象。 DOM中的事件对象兼容DOM的浏览器会将一个event对象传入到事件处理程序中。1234var btn = document.getElementById("myBtn");btn.onclick = function(event) { alert(event.type); //“click”}; 在事件处理程序内部,对象this始终等于currentTarget的值,而target则包含事件的实际目标。 IE中的事件对象要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法。在使用DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在。12345var btn = document.getElementById("myBtn");btn.onclick = function(){ var event = window.event; alert(event.type); //“click”}; 因为事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为this会始终等于事件目标。所以,最好还是使用event.srcElement来确定事件目标。 跨浏览器的事件对象对前面的EventUtil对象加以增强:12345678910111213141516171819202122232425262728293031323334353637383940EventUtil = { addHandler : function(element,type,handler) { if(element.addEventListener) { element.addEventListener(type,handler,false); }else if(element.attachEvent) { element.attachEvent("on"+type,handler); }else { element["on"+type] = handler; } }, getEvent: function(event) { return event ? event : window.event; }, getTarget: function(event) { return event.target || event.srcElement; }, preventDefault:function(event) { if(event.preventDefault) { event.preventDefault(); }else { event.returnValue=true; } }, removeHandler: function(element,type,handler) { if(element.removeEventListener) { element.removeEventListener(type,handler,false); }else if(element.detachEvent) { element.detachEvent("on"+type,handler); }else { element["on"+type] = null; } }, stopPropagation: function(event) { if(event.stopPropagation) { event.stopPropagation(); }else{ event.cancelBubble = true; } }}; 事件类型“DOM3级事件”规定了以下几类事件:UI事件:当用户与页面上的元素进行交互时触发;UI事件(在DOM2级中归为HTML事件)有:load、unload、abort、error、select、resize、scroll 焦点事件:当元素获得或是去焦点时触发;blur、focusin、focusout、focus、DOMFocusIn、DOMFocusOut;利用这些事件并与document.hasFocus()方法及document.activeElement属性配合,可以知晓用户在页面上的行踪。 当焦点从页面中的一个元素移动到另一个元素,会依次触发下列事件:1)focusout在失去焦点的元素上触发;2)focusin在获得焦点的元素上触发;3)blur在失去焦点的元素上触发;4)DOMFocusOut在失去焦点的元素上触发;5)focus在获得焦点的元素上触发;6)DOMFocusIn在获得焦点的元素上触发。 鼠标事件:当用户通过鼠标在页面上执行操作时触发;click、dbclick、mousedown、mouseenter、mouseleave、mousemove、mouseout、mouseover、mouseup 相关元素:event对象的relatedTarget属性提供了相关元素的信息。例如mouserover,事件的主目标是获得光标的元素,而相关元素就是那个失去光标的元素。 滚轮事件:当使用鼠标滚轮(或类似设备)时触发;mousewheel;与mousewheel事件对应的event对象除包含鼠标事件的所有标准信息外,还包括一个特殊的wheelDelta属性。当用户向前滚动鼠标滚轮时,wheelDelta是120的倍数;当用户向后滚动鼠标滚轮时,wheelDelta是-120的倍数。 而FireFox支持名为DOMMouseScroll的类似事件,有关鼠标滚轮的信息则保存在detail属性中,向前滚动鼠标滚轮时,属性值为-3的倍数,向后滚动鼠标滚轮时,属性值为3的倍数。 文本事件:当在文档中输入文本时触发;键盘事件:当用户通过键盘在页面上执行操作时触发;keydown(任意键)、keypress(仅字符键)、keyup;在发生keydown和keyup事件时,event对象的keyCode属性会包含一个代码,与键盘上一个特定的键对应。 要想以跨浏览器的方式取得字符编码,必须首先检测charCode属性是否可用,如果不可用则使用keyCode.1234567891011var EventUtil = { //省略代码 getCharCode : function(event){ if(typeof event.charCode == "number"){ return event.charCode; }else{ return event.keyCode; } } //省略代码} 合成事件(DOM3级):当为IME(输入法编辑器)输入字符时触发;变动事件:当底层DOM结构发生变化时触发;变动名称事件:当元素或属性名变动时触发。(已被废弃) HTML5事件1.contextmenu事件通常使用contextmenu事件来显示自定义的上下文菜单。 2.beforeunload事件该事件是为了让开发人员有可能在页面卸载前阻止这一操作。 3.DOMContentLoaded事件该事件在形成完整的DOM树之后触发,不理会图像、javascript文件、css文件或其他资源是否已经下载完毕。 4.readystatechange事件该事件的目的是提供与文档或元素的加载状态有关的信息。支持readystatechange事件的每个对象都有一个readyState属性,可能包含下列值:uninitialized(未初始化):对象存在但尚未初始化;loading:对象正在加载数据;loaded:对象加载数据完成;interactive(交互):可以操作对象了,但还没有完全加载;complete:对象加载完毕。 5.pageshow和pagehide事件 6.hashchange事件将hashchange事件处理程序添加给window对象,然后URL参数列表只要变化就会调用它。此时event对象应该额外包含两个属性:oldURL和newURL。这两个属性分别保存着参数列表变化前后的完整URL。 设备事件1.orientationchange事件苹果公司为移动Safari中添加了orientationchange事件,以便开发人员能够确定用户何时将设备由横向查看模式切换为纵向查看模式。window.orientation属性可能包含3个值:0表示肖像模式,90表示向左旋转的横向模式,-90表示向右旋转的横向模式。 2.MozOrientation事件Firefox3.6为检测设备的方向引入了一个名为MozOrientation的新事件。 3.deviceorientation事件 4.devicemotion事件 触摸与手势事件1.触摸事件touchstart:当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。touchmove:当手指在屏幕上滑动时连续地触发。这个事件发生期间,调用preventDefault()可以阻止滚动touchend:当手指从屏幕移开时触发;touchcancel:当系统停止跟踪触摸时触发;三个用于跟踪触摸的属性:touches:表示当前跟踪的触摸操作的Touch对象的数组;targetTouches:特定于事件目标的Touch对象的数组;changeTouches:表示自上次触摸以来发生了什么改变的Touch对象的数组。 每个Touch对象包含下列属性:clientX:触摸目标在视口中的x坐标;clientY:触摸目标在视口中的y坐标identifier:标识触摸的唯一ID;pageX:触摸目标在页面中的x坐标;pageY:触摸目标在页面中的y坐标;screenX:触摸目标在屏幕中的x坐标;screenY:触摸目标在屏幕中的y坐标;target:触摸的DOM节点目标。 2.手势事件gesturestart:当一个手指已经按在屏幕上而另一个手指又触摸屏幕时触发。gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发。gestureend:当任何一个手指从屏幕上面移开时触发。 内存和性能事件委托(事件代理)事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。优点: document对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待DOMContentLoaded或load事件)。 在页面中设置事件处理程序所需的时间更少。只添加一个事件处理程序所需的DOM引用更少,所花时间更少。 整个页面占用的内存空间更少,能够提升整体性能。 移除事件处理程序每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的Javascript代码之间就会建立一个连接。连接越多,页面执行起来就慢。所以,需要将那些过时不用的“空事件处理程序”移除。 当从文档中移除带有事件处理程序的元素时,原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。所以最好手工移除事件处理程序,eg : btn.onclick = null; 导致“空事件处理程序”的另外一种情况,就是卸载页面的时候。如果在页面被卸载之前没有清理干净事件处理程序,那它们会滞留在内存中。所以,最好的做法是在页面卸载之前,先通过onunload事件处理程序移除所有的事件处理程序。 模拟事件DOM中的事件模拟通过document.createEvent()方法创建event对象,这个方法接收一个参数,即表示要创建的事件类型的字符串。字符串如下:UIEvents :一般化的UI事件。MouseEvents : 一般化的鼠标事件。MutationEvents : 一般化的DOM变动事件。HTMLEvents : 一般化的HTML事件。 模拟事件的触发需要使用dispatchEvent()方法,所有支持事件的DOM节点都支持这个方法。 1.模拟鼠标事件(MouseEvent)1234567var btn = document.getElementById("myBtn");//创建事件对象var event = document.createEvent("MouseEvent");//初始化事件对象event.initMouseEvent("click",true,true,document.defaultView,0,0,0,0,0,false,false,false,false,0,null);//触发事件btn.dispatchEvent(event); 2.模拟键盘事件(keyboardEvent)123456789var textbox = document.getElementById("myTextbox"), event;//以DOM3级方式创建事件对象if(document.implementation.hasFeature("KeyboardEvents","3.0")){ event = document.createEvent("KeyboardEvent"); //模拟按下Shift键又同时按下A键 event.initKeyboardEvent("keydown",true,true,document.defaultView,"a",0,"Shift",0); }textbox.dispatchEvent(event); IE中的事件模拟通过document.createEventObject()在IE中创建event对象,这个方法不接受参数,返回通用的event对象。然后,你必须手工为这个对象添加所有必要的信息,最后在目标上调用fireEvent()方法。fireEvent()接受两个参数:事件处理程序名称和event对象。12345678910var btn = document.getElementById("myBtn");//创建事件对象var event = document.createEventObject();//初始化事件对象event.screenX=100;event.screenY=0;//...省略其他属性的初始化//触发事件btn.fireEvent("onclick",event); 表单脚本表单的基础知识在Javascript中,表单对应的是HTMLFormElement类型,继承于HTMLElement。有如下属性和方法:acceptCharset:服务器能够处理的字符集;action:接受请求的URL;elements:表单中所有控件的集合;enctype:请求的编码类型;length:表单中控件的数量;method:要发送的HTTP请求类型;name:表单的名称;reset():将所有表单域重置为默认值;submit():提交表单;target:用于发送请求和接收响应的窗口名称。 取得表单主要是通过为其添加id特性,然后使用document.getElementById()方法取得;此外还可以通过document.forms可以取得页面中所有的表单。12var firstForm = document.forms[0]; //通过数值索引获取var myForm = document.forms["myForm"]; //通过name值获取 提交表单用户点击提交按钮或图像按钮时,就会提交表单。图像按钮:1<input type = "image" src = "img.gif"/> 浏览器会在将请求发送给服务器之前触发submit事件,这样,我们就有机会验证表单数据,并据以决定是否允许表单提交。阻止这个事件的默认行为就可以取消表单那提交。1234567891011121314var form = document.getElementById("myForm");EventUtil.addHandler(form,"submit",function(event){ event = EventUtil.getEvent(event); EventUtil.preventDefault(event);});在Javascript中,以编程方式调用submit()方法提交表单。eg:var form = document.getElementById("myForm");form.submit();// 重置表单form.reset(); 表单字段123var form = document.getElementById("myForm");var field1 = form.elements[0];var field2 = form.elements["textbox2"]; 如果有多个表单控件都在使用一个name(如单选按钮),name就会返回以该name命名的一个NodeList。 共有的表单字段属性disabled,form,name,readonly,tabIndex,type,value。 为防止用户重复点击表单的提交按钮,需要在第一次单击后就禁用提交按钮。1234567EventUtil.addHandler(form, "submit", function(event) { event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); var btn = target.elements["submit-btn"]; btn.disabled = true;}); 共有的表单字段方法focus()和blur()。在HTML5中,为表单字段新增了一个autofocus属性,在支持这个属性的浏览器中,只要设置了,不用Javascript就能自动把焦点移动到相应字段。1<input type="text" autofocus /> 共有的表单字段事件blur、change、focus。1234567891011var textbox = document.forms[0].elements[0];EventUtil.addHandler(textbox,"change",function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); if(/[^\d]/.test(target.value)){ target.style.backgroundColor = "red"; }else{ target.style.backgroundColor = ""; }}); 文本框脚本<input>和<textarea>要实现文本框,必须将<input>元素的type特性设置为”text”。而通过设置size特性,可以指定文本框中能够显示的字符数。而maxlength特性则用于指定文本框可以接受的最大字符数。 <textarea>则始终会呈现一个多行文本框。通过使用rows(行)和cols(列)特性来指定文本框大小 选择文本上述两种文本框都支持select()方法,这个方法用于选择文本框中的所有文本,该方法不接受参数。1.选择(select)事件在大多数浏览器中,只有用户选择了文本(而且释放鼠标),才会触发select事件。而在IE8及更早版本中,只要用户选择了一个字母(不必释放鼠标),就会触发select事件。2.取得选择的文本在HTML5中,添加了两个属性:selectionStart和selectionEnd。这两个属性中保存的是基于0的数值,表示所选择的文本的范围。因此,要取得用户在文本框中选择的文本,可以如下编码:123function getSelectedText(textbox){ return textbox.value.substring(textbox.selectionStart,textbox.selectionEnd);} 而IE8及其更早版本要实现取得用户选择的文本,需要通过document.selection对象,其中保存着用户在整个文档范围内选择的文本信息。12345678//兼容各浏览器function getSelectedText(textbox){ if(typeof textbox.selectionStart == "number"){ return textbox.value.substring(textbox.selectionStart,textbox.selectionEnd); }else if(document.selection){ return document.selection.createRange().text; }} 3.取得部分文本setSelectionRange()方法,所有文本框都支持,该方法接受两个参数:要选择的第一个字符的索引和要选择的最后一个字符之后的字符的索引。12345textbox.value="Hello World!";//选择所有文本textbox.setSelectionRange(0,textbox.value.length); //"Hello World!"//选择前三个字符textbox.setSelectionRange(0,3); //"Hel" 要看到选择的文本,必须在调用setSelectionRange()之前或之后立即将焦点设置到文本框。IE9、FireFox、Safari、Chrome和Opera支持这种方案。而IE8及其更早版本支持使用范围选择部分文本。1234567891011121314//兼容各浏览器版function selectText(textbox,startIndex,stopIndex){ if(textbox.setSelectionRange){ textbox.setSelectionRange(startIndex,stopIndex); }else if(text.createTextRange){ var range = textbox.createTextRange(); range.collapse(true); //将范围折叠到文本框的开始位置 range.moveStart("character",startIndex); range.moveEnd("character",stopIndex-startIndex); range.select(); } textbox.focus(); //设置文本框焦点,以便用户看到文本框中选择的文本}selectText(textbox,0,textbox.value.length); 过滤输入1.屏蔽字符检测keypress事件对应的字符编码,然后再决定如何响应。EventUtil.addHandler(textbox, “keypress”, function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); var charCode = EventUtil.getCharCode(event); //屏蔽非数值,并避免屏蔽一些常用和必要的键,,如向上键、向下键、ctrl+C等 if(!/\d/.test(String.fromCharCode(charCode))&&charCode>9&&!event.ctrlKey){ EventUtil.preventDefault(event); }}); 2.操作剪贴板六个剪贴板事件:beforecopy:在发生复制操作前触发。copy:在发生复制操作时触发。beforecut:在发生剪切操作前触发。cut:在发生剪切操作时触发。beforepaste:在发生粘贴操作前触发。paste:在发生粘贴操作时触发。 要访问剪切板中的数据,可以使用clipboardData对象:在IE中,这个对象是window对象的属性;而在其他主流浏览器中,这个对象是相应的event对象的属性。clipboardData对象有三个方法:getData()、setData()和clearData()。这三个方法的接受参数在IE和其他主流浏览器中有所区别。1234567891011121314var EventUtil={ //省略的代码 getClipboardText: function(event){ var clipboardData = (event.clipboardData || window.clipboardData); return clipboardData.getData("text"); } , setClipboardText: function(event,value){ if(event.clipboardData){ return event.clipboardData.setData("text/plain",value); }else if(window.clipboardData){ return window.clipboardData.setData("text",value); } }}; 在需要确保粘贴到文本框中的文本包含某些字符,或者符合某种格式要求时,能够访问剪贴板是非常有用的。1234567EventUtil.addHandler(textbox, "paste", function(event){ event = EventUtil.getEvent(event); var text = EventUtil.getClipboardText(event); if(!/^\d*$/.test(text)){ EventUtil.preventDefault(event); }}); 自动切换焦点为增强表单字段的易用性,通常在用户填写完当前字段时,自动将焦点切换到下一个字段。123<input type="text" name="tel1" id="txtTel1" maxlength="3"><input type="text" name="tel2" id="txtTel2" maxlength="3"><input type="text" name="tel3" id="txtTel3" maxlength="4"> 当用户在前一个文本框中输入的字符达到最大数量后,自动将焦点切换到下一个文本框。12345678910111213141516171819202122232425(function(){ function tabForward(event){ evnet = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); if (target.value.length == target.maxlength){ var form = target.form; for (var i = 0,len = form.elements.length;i<len; i++) { if(form.elements[i] == target){ if(form.elements[i+1]){ form.elements[i+1].focus(); } return; } } } } var textbox1 = document.getElementById("txtTel1"); var textbox2 = document.getElementById("txtTel2"); var textbox3 = document.getElementById("txtTel3"); EventUtil.addHandler(textbox1,"kepup",tabForward); EventUtil.addHandler(textbox2,"kepup",tabForward); EventUtil.addHandler(textbox3,"kepup",tabForward);})(); HTML5约束验证API当用户禁用javascript或浏览器不支持时,使用HTML5提供的一些功能,仍能够实现基本的验证。1.必填字段required属性1<input type="text" required> 2.其他输入类型HTML5为<input>元素的type属性又增加了几个值:email和url等。12<input type="email" name="email"><input type="url" name="homepage"> 3.数值范围除了”email”和“url”,HTML5还定义了另外几个输入元素:”number”、”range”、”datetime”、”datetime-local”、”date”、”month”、”week”以及”time”.对这些数值类型的输入元素,可以指定min属性、max属性和step属性。此外,还有两个方法:stepUp()和stepDown(),都可以接收一个可选的参数:要在当前值基础上加上减去的数值。(默认是加或减1)这两个方法都还没得到任何浏览器的支持。 4.输入模式HTML5为文本字段新增了pattern属性,这个属性的值是一个正则表达式,用于匹配文本框中的值。只允许输入数值:1<input type ="text" pattern="\d+" name="count"> 注意,模式的开头和结尾不用加^和$符号。这两个符号表示输入的值必须从头到尾都与模式匹配。 5.检测有效性使用checkValidity()方法可以检测表单中的某个字段是否有效。如果字段有效,返回true,否则返回false。字段的值是否有效的判断依据是根据前面介绍的各种约束,如必填字段如果没有值就是无效,而字段中的值与pattern属性不匹配也是无效的。12345if(document.forms[0].elements[0].checkValidity()){//字段有效}else{//字段无效} validity属性会告知什么字段有效或无效。这个对象有一系列属性,每个属性返回布尔值。customError、patternMismatch、rangeOverflow、rangeUnderflow、stepMismatch、tooLong、typeMismatch、valid、valueMissing。 6.禁用验证通过设置novalidate属性,可以告诉表单不进行验证。 选择框脚本选择框是通过<select>和<option>元素创建的。有如下的属性和方法:add(newOption,relOption):向控件中插入新<option>元素,其位置在相关项(relOption)之前。multiple:布尔值,表示是否允许多选。options:控件中所有<option>元素的HTMLCollection。remove(index):移除给定位置的选项。selectIndex:基于0的选中项的索引,如果没有选中项,则值为-1; 对于支持多选的控件,只保存选中项中第一项的索引。size:选择框中可见的行数。 在DOM中,每个<option>元素都有一个HTMLOptionElement对象表示,并拥有如下的属性:index:当前选项在options 集合中的索引。label:当前选项的标签;selected:布尔值,表示当前选项是否被选中。将这个属性设置为true可以选中当前选项。text:选项的文本;value:选项的值。 选择选项对于单选的选择框,访问选中项的最简单方式,就是使用选择框的selectedIndex 属性。1var selectedOption = selectbox.options[selectbox.selectedIndex]; 另一种选择选项的方式,就是取得对某一项的引用,然后将其属性设置为true。1selectbox.options[0].selected=true; selected属性的作用主要是确定用户选择了选择框中的哪一项。 添加选项有多种添加选项的方式,如下:1234var newOption = document.createElement("option");newOption.appendChild(document.createTextNode("Option text"));newOption.setAttribute("value","Option value");selectbox.appendChild(newOption); 第二种方式是使用Option构造函数来创建新选项。Option构造函数接收两个参数:文本(text)和值(value)。12var newOption = new Option("Option text","Option value");selectbox.appendChild(newOption); 第三种添加新选项的方式是使用选择框的add()方法。该方法接受两个参数:要添加的新选项和将位于新选项之后的选项。123var newOption = new Option("Option text","Option value");//第二个参数传入undefined,表示将新选项插入到列表最后。这是最佳方案selectbox.add(newOption,undefined); 要想将新选项插入到其他位置,应该使用标准的DOM技术和insertBefore()方法。 移除选项使用DOM的removeChild()方法或者使用选择框的remove()方法,还有最后一种就是将相应选项设置为null。123selectbox.removeChild(seclectbox.options[0]); //移除第一个选项selectbox.remove(0); //传入要移除选项的索引selectbox.options[0]=null; 移动和重排选项使用DOM技术能够很简单的实现移动和重排选项。12345678//移动选项,完成后会重置每一个选项的index属性var selectbox1 = document.getElementById("selLocations1");var selectbox2 = document.getElementById("selLocations2");selectbox2.appendChid(selectbox1.options[0]);//重排选项var optionToMove = selectbox.options[1];selectbox.insertBefore(optionToMove,selectbox.options[optionToMove.index-1]); 表单序列化浏览器将数据发送给服务器的过程:对表单字段的名称和值进行URL编码,使用和号(&)分隔;不发送禁用的表单字段;只发送勾选的复选框和单选按钮;不发送type为“reset”和“button”的按钮;多选选择框中的每个选中的值单独一个条目;在单击提交按钮提交表单的情况下,也会发送提交按钮;否则不发送提交按钮。<select>元素的值,就是选中的<option>元素的value特性的值。如果<option>元素没有value特性,则是<option>元素的文本值。 表单序列化的实现:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475function (form){ var parts=[], field = null, i, len, j, optLen, option, optValue; for (i = 0,len = form.elements.length; i < len; i++) { field = form.elements[i]; switch(field.type){ case "select-one": case "select-multiple": if (field.name.length) { for (j = 0,optLen=field.options.length; j < optLen; j++) { option = field.options[j]; if (option.selected) { optValue=""; if (option.hasAttribute) { optValue = (option.hasAttribute("value") ? option.value : option.text); }else{ //兼容IE optValue = (option.attributes["value"].specified ? option.value : option.text); } parts.push(encodeURIComponent(field.name)+ "=" + encodeURIComponent(field.value)); } } } break; case undefined://字段集 case "file": case "submit": case "reset": case "button": break; case "radio": case "checkbox": if (!field.checked) { break; } default: //不包含没有名字的表单字段 if(field.name.length){ parts.push(encodeURIComponent(field.name)+ "=" + encodeURIComponent(field.value)); } } } return parts.join("&"); }## 富文本编辑富文本编辑,又称为WYSIWYG(What You See Is What You Get,所见即所得)。这一技术的本质,就是在页面中嵌入一个包含空的HTML页面的iframe。通过设置designMode属性,这个空白的HTML页面可以被编辑,而编辑的对象则是该页面&lt;body&gt;元素的HTML代码。designMode属性有两个可能的值:“off”(默认值)和“on”。在设置为“on”时,整个文档都会变得可以编辑。使用contenteditable属性把contenteditable属性应用给页面中的任何元素,然后用户立即就可以编辑该元素。contenteditable 属性有三个可能的值:“true"表示打开,"false"表示关闭,"inherit"表示从父元素那里继承。**操作富文本**与富文本编辑器交互的主要方式,就是使用document.execCommand()。这个方法可以对文档执行预定义的命令,而且可以应用大多数格式。该刚方法传递三个参数:要执行的命令名称、表示浏览器是否应该为当前命令提供用户界面的一个布尔值和执行命令必须一个值(如果不需要则传递null)。为确保跨浏览器的兼容性,第二个参数应该始终设置为false。调用execCommand()可以实现浏览器菜单的很多功能. 如保存文件,打开新文件,撤消、重做操作…等等```js//复制document.execCommand("copy",false,null);//剪切document.execCommand("cut",false,null);//粘贴document.execCommand("paste",false,null);//打印document.execCommand("print",false,null); 富文本选区在富文本编辑器中,使用框架的getSelection()方法,可以确定实际选择的文本。这个方法是window对象和document对象的属性,调用它会返回一个表示当前选择文本的Selection对象。每个Selection对象都有以下属性:anchorNode:选区起点所在的节点。anchorOffset:在到达选区起点位置之前跳过anchorNode中的字符数量。focusNode:选区终点所在的节点。focusOffset:focusNode中包含在选区之内的字符数量。isCollapsed:布尔值,表示选区的起点和终点是否重合。rangeCount:选区中包含的DOM范围的数量。上述属性并没有多少用处,但以下的方法提供了更多的信息,并支持对选区的操作。addRange(range):将指定的DOM范围添加到选区中。collapse(nde,offset):将选区折叠到指定节点中的相应的文本偏移位置。collapseToEnd():将选区折叠到终点位置。collapseToStart():将选区折叠到起点位置。containsNode(node):确定指定的节点是否包含在选区中。deleteFromDocument():从文档中删除选区中的文本,与document。execCommand(“delete”,false,null)命令的结果相同。extend(node,offset):通过将focusNode和focusOffset移动到指定的值来扩展选区。getRangeAt(index):返回索引对应选区中的DOM范围。removeAllRanges():从选区中移除所有DOM范围。实际上,这样会移除选区,因为选区中至少要有一个范围。removeRange(range):从选区中移除指定的DOM范围。selectAllChildren(node):清除选区并选择指定节点的所有子节点。toString():返回选区所包含的文本内容。下面来看一个例子:123456789var selection =frames["richedit"].getSelection();//取得选择的文本var selectedText = selection.toString();//取得代表选区的范围var range = selection.getRangeAt(0);//突出显示选择的文本var span = frames["richedit"].document.createElement("span");span.style.backgroundColor = "yellow";range.surroundContents(span); 表单与富文本富文本编辑器是使用iframe而非表单控件实现的,富文本编辑器中的HTML不会被自动提交到服务器,而需要我们手工来提取并提交HTML。123456EventUtil.addHandler(form,"submit",function(event){ event = EventUtil.getEvent(event); var target=EventUtil.getTarget(event); target.elements["comments"].value = frames["richedit"].document.body.innerHTML;}); 通过文档主体的innerHTML属性获取了iframe中的HTML,然后将其插入到了名为”comments”的表单字段中。]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(四)]]></title>
<url>%2F2017%2F05%2F14%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E5%9B%9B%EF%BC%89%2F</url>
<content type="text"><![CDATA[DOM操作技术1.动态脚本创建动态脚本的方式:插入外部文件和直接插入javascript代码在一般浏览器中,可以使用DOM操作生成<script>标签以及通过createTextNode()方法创建其子节点,但IE中,将<script>视为一个特殊元素,不允许DOM访问其子节点,但可以使用<script>标签的text属性来指定javascript代码:12345678910111213141516var script = document.createElement("script");script.type = "text/javascript";script.text = "function sayHi(){alert('hi');}";document.body.appendChild(script);//动态添加脚本的通用函数function loadScriptString(code) { var script = document.createElement("script"); script.type = "text/javascript"; try { script.appendChild(document.createTextNode(code)); }catch(ex) { script.text =code; } document.body.appendChild(script);} 2.动态样式创建动态样式的方式:通过<link>标签引入外部css文件和通过<style>标签来嵌入CSS代码动态创建<link>标签时,必须要注意的是,要将<link>标签添加到<head>中而不是<body>IE中,也将<style>视为一个特殊元素,不允许DOM访问其子节点。解决这个问题的方法,就是访问元素的styleSheet属性,该属性又有一个cssText属性,可以接受css代码:12345678910111213141516171819202122var style = document.createElement("style");style.type = "text/css";try{ style.appendChild(document.createTextNode("body{background-color: red}"));}catch(ex){ style.styleSheet.cssText = "body{background-color: red}";}var head =document.getElementsByTagName("head")[0];head.appendChild(style);//动态添加样式的通用函数function loadStyleString(css){ var style = document.createElement("style"); style.type = "text/css"; try{ style.appendChild(document.createTextNode(css)); }catch(ex){ style.styleSheet.cssText = css; } var head =document.getElementsByTagName("head")[0]; head.appendChild(style);} 3.操作表格<table>元素是HTML中最复杂的结构之一。为了方便构建表格,HTML DOM还为<table>、<tbody>和<tr>元素添加了一些属性和方法。详情:http://www.w3school.com.cn/jsref/dom_obj_table.asp 4.使用NodeList应该尽量减少访问NodeList的次数 DOM扩展对DOM的两个主要扩展是Selectors API(选择符API)和HTML5。此外,还有一个不那么引人注目的Element Traversal(元素遍历)规范和一些DOM的专有扩展。 众多Javascript库中最常用的一项功能,就是根据CSS选择符选择与某个模式匹配的DOM元素。jQuery的核心就是通过CSS选择符查询DOM文档取得元素的引用,从而抛开了getElementById()和getElementsByTagName()。 选择符APISelectors API Level 1的核心是两个方法:querySelector()和querySelectorAll()。(IE8以下浏览器不支持) querySelector()接收一个CSS选择符,返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回null。1234var body = document.querySelector("body");var myDiv = document.querySelector("#myDiv");var selected = document.querySelector(".selected");var img = document.body.querySelector("img.button"); querySelectorAll()方法接收的参数与querySelector()一样,都是一个CSS选择符,但返回的是所有匹配的元素而不仅仅一个元素,返回的是一个NodeList对象,如果没有找到匹配元素,NodeList就是空。 Selectors API Level 2规范为Element类型新增了一个方法matchesSelector()。这个方法接收一个参数,即CSS选择符,如果调用元素与该选择符匹配,则返回true,否则,返回false.123if(document.body.matchesSelector("body.page1")){ //true //do something} 该方法还未达成统一标准:IE9+通过msMatchesSelector()支持,Firefox 3.6+通过mozMatchesSelector()支持,Safari 5+和Chrome通过webkitMatchesSelector()支持。编写一个兼容的包装函数:12345678910111213function matchesSelector(element,selector){ if (element.matchesSelector){ return element.matchesSelector(selector); }else if(element.msMatchesSelector){ return element.msMatchesSelector(selector) }else if(element.mozMatchesSelector){ return element.mozMatchesSelector(selector); }else if(element.webkitMatchesSelector){ return element.webkitMatchesSelector(selector); }else{ throw new Error("Not supported."); }} 元素遍历Element Traversal API为DOM元素添加了以下5个属性:childElementCount:返回子元素(不包括文本节点和注释)的个数;firstElementChild:指向第一个子元素;firstChild的元素版。lastElementChild:指向最后一个子元素;lastChild的元素版。previousElementSibling:指向前一个同辈元素;previousSibling的元素版。nextElementSibling:指向后一个同辈元素;nextSibling的元素版。 利用这些元素就可以不必担心空白文本节点,从而更方便地查找DOM元素。 HTML5与类相关的扩充1.getElementsByClassName()方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的NodeList。传入多个类名时,类名的先后顺序不重要。2.classList属性(仅Firefox3.6+和Chrome支持)classList属性是新集合类型DOMTokenList的实例。新类型有如下方法:add(value):将给定的字符串值添加到列表中。如果值已存在,就不添加了。contains(value):表示列表中是否存在给定的值,如果存在返回true,反之,false。remove(value):从列表中删除给定的字符串。toggle(value):如果列表中已经存在给定值,删除它,如果列表中没有给定值,则添加它。123<div class="bd user disabled"></div>//移除“user”类div.classList.remove("user"); 焦点管理HTML5添加了辅助管理DOM焦点功能。document.activeElement属性,这个属性始终会引用DOM中当前获得焦点的元素。 元素获得焦点的方式有页面加载、用户输入和代码中调用focus()方法。默认情况下,页面刚刚加载完成时,document.activeElement中保存的是document.body元素的引用。文档加载期间,document.activeElement的值为null document.hasFocus()方法,用于确定文档是否获得了焦点。通过检测文档是否获得了焦点,可以知道用户是不是正在与页面交互。查询文档获知哪个元素获得了焦点,以及确定文档是否获得了焦点,这两个功能最重要的用途是提高web应用的无障碍性。 HTMLDocument的变化1.readyState属性(document.ready)该属性有两个值:loading(正在加载文档)和complete(已经加载完文档)2.兼容模式(document.compatMode)自从IE6开始区分渲染页面的模式是标准的还是混杂的,检测页面的兼容模式就成为了浏览器的必要功能。document.compatMode,这个属性就是为了告诉开发人员浏览器采用了哪种渲染模式。在标准模式下:document.compatMode == “CSS1Compat”在混杂模式下:document.compatMode == “BackCompat”3.head属性(仅Chrome和Safari5+支持)HTML5新增了document.head属性,引用文档的<head>元素1var head =document.head||document.getElementsByTagName("head")[0]; 字符集属性document.charset,可以通过这个属性获取或修改charset属性的值。 自定义数据属性HTML5规定可以为元素添加非标准的属性,但要添加前缀data-,目的是为元素提供与渲染无关的信息,或者提供语义信息。可以通过dataset属性来访问自定义属性的值。dataset属性的值是DOMStringMap的一个实例,也就是一个名值对儿的映射。123456789<div id="myDiv" data-appId="12345" data-myname="Peter"></div>var div = document.getElementById("myDiv");//取得自定义属性的值var appId = div.dataset.appId;var myname=div.dataset.myname;//设置值div.dataset.appId=23456;div.dataset.myname="Bob"; 插入标记1.innerHTML属性在读模式下,innerHTML属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的HTML标记。在写模式下,innerHTML会根据指定的值创建新的DOM树,然后用这个DOM树完全替换调用元素原先的所有子节点2.outerHTML属性在读模式下,outerHTML属性返回调用它的元素及所有子节点的HTML标记。在写模式下,outerHTML会根据指定的HTML字符串创建新的DOM子树,然后用这个DOM子树完全替换调用元素。3.insertAdjacentHTML()方法这个方法最早出现在IE,它接收两个参数:插入位置和要插入的HTML文本。第一个参数必须是下列的值:beforebegin、afterbegin、beforeend、afterend4.内存和性能问题替换节点可能会导致浏览器的内存占用问题。 scrollIntoView()方法(Chrome不支持)该方法可以在所有HTML元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。 专有扩展(浏览器各自专有的扩展,并未写进标准)1.文档模式,IE8引入的,主要有四种:IE5:混杂模式;IE7:以IE7标准模式渲染页面;IE8:以IE8标准模式渲染页面;IE9(最高级):以IE9标准模式渲染页面;2.children属性这个属性是HTMLCollection的实例,只包含元素中同样还是元素的子节点。3.contains()方法判断某个节点是不是另一个节点的后代。4.插入文本innerText和outerText5.滚动scrollIntoViewIfNeeded(alignCenter)、scrollByLines(lineCount)、scrollByPages(pageCount) DOM2和DOM3DOM1级主要定义的是HTML和XML文档的底层结构。DOM2和DOM3级则在这个结构的基础上引入了更多的交互能力,也支持了更高级的XML特性。DOM2级的主要模块: DOM2级核心:在1级核心基础上构建,为节点添加了更多方法和属性。 DOM2级视图:为文档定义了基于样式信息的不同视图。 DOM2级事件:说明了如何使用事件与DOM文档交互。 DOM2级样式:定义了如何以编程方式来访问和改变CSS样式信息。 DOM2级遍历和范围:引入了遍历DOM文档和选择其特定部分的新接口。 DOM2级HTML:在1级HTML基础上构建,添加了更多属性、方法和新接口。 DOM变化DOM2级和3级的目的在于扩展DOM API,以满足操作XML的所有需求,同时提供更好的错误处理和特性检测能力。DOM3级引入了两个辅助比较节点的方法:isSameNode()和isEqualNode(),这两个方法都接收一个节点参数。DOM3级还针对DOM节点添加额外数据引入了新方法:setUserData()和getUserData()。setUserData()接收三个参数1234567891011document.body.setUserData("name","Peter",function(){});var div = document.createElement("div");div.setUserData("name","Peter",function(operation,key,value,src,dest) { if(operation == 1) { dest.setUserData(key,value,function(){}); }});var newDIiv = div.cloneNode(true);alert(newDiv.getUserData("name")); //Peter 框架的变化框架和内嵌框架分别用HTMLFrameElement和HTMLIFrameElement表示,它们在DOM2级有个新属性,contentDocument。这个属性包含一个指针,指向表示框架内容的文档对象。12var iframe = document.getElementById("myIframe");var iframeDoc = iframe.contentDocument;(IE8之前版本不支持) IE8之前版本支持contentWindow的属性,该属性返回框架的window对象,而这个window对象又有一个document属性。因此,要想在所有浏览器中访问内嵌框架的文档对象,可以使用以下代码:12var iframe = document.getElementById("myIframe");var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; 样式在HTML中定义样式有三种方法:通过<link/>元素包含外部样式表文件、使用<style/>元素定义嵌入式样式,以及使用style特性针对特定元素的样式。检测浏览器是否支持DOM2级定义的CSS能力:12var supportDOM2CSS = document.implementation.hasFeature("CSS","2.0");var supportDOM2CSS = document.implementation.hasFeature("CSS2","2.0"); 访问元素的样式任何支持style特性的HTML元素在javascript中都有一个对应的style属性。通过style属性设置或获取元素的样式。 1.DOM样式属性和方法”DOM2级样式“规范为style对象定义了如下的属性和方法:cssText:通过它能够访问到style特性中的CSS代码length:应用给元素的CSS属性的数量parentRule:表示CSSRule对象。getPropertyCSSValue(propertyName):返回给定属性值的CSSValue对象,该对象包含两个属性:cssText和cssValueType。cssValueType属性是一个数值常量,表示值的类型:0表示继承的值,1表示基本的值,2表示值列表,3表示自定义的值。 getPropertyPriority(propertyName):如果给定的属性使用了!important设置,则返回”important”;否则,返回空字符串。getPropertyValue(propertyName):返回给定属性的字符串值。item(index):返回给定位置的CSS属性的名称。removeProperty(propertyName):从样式中删除给定属性。setProperty(propertyName,value,priority):将给定属性设置为相应的值,并加上优先权标志(“important”或者空字符串)。 2.操作样式表CSSStyleSheet类型表示的是样式表,包括通过<link>元素包含的样式表和<style>中定义的样式表。CSSStyle继承自StyleSheet,有如下的属性:disabled:表示样式是否被禁用的布尔值。·,,href:如果样式表通过<link>包含的,则是样式表的URL;否则,是null。meida:当前样式表所支持的所有媒体类型的集合。ownerNode:指向拥有当前样式表的节点的指针,样式可能是在HTML中通过<link>或<style>引入的。如果当前样式表是其他样式表通过@import导入的,这个属性值为null。IE不支持这个属性。parentStyleSheet:在当前样式表是通过@import导入的情况下,这个属性是一个指向导入它的样式表的指针。title:ownerNode中的title属性的值。type:表示样式表类型的字符串。通常是“type/css”cssRules:样式表中包含的样式规则的集合。IE不支持ownerRule:如果样式表是通过@import导入的,这个属性就是一个指针,指向表示导入的规则,否则,值为null。IE不支持deleteRule(index):删除cssRules集合中指定位置的规则。IE不支持inserRule(rule,index):向cssRules集合中指定的位置插入rule字符串。IE不支持 3.元素大小1)偏移量offsetHeight:元素在垂直方向上占用的空间大小。offsetWidth:元素在水平方向上占用的空间大小。offsetLeft:元素的左外边框至包含元素的左内边框之间的像素距离。offsetTop:元素的上外边框至包含元素的上内边框之间的像素距离。 2)客户区大小clientWidth:元素内容区宽度加上左右内边距宽度;clientHeight:元素内容区高度加上上下内边距宽度; 3)滚动大小scrollHeight:在没有滚动条的情况下,元素内容的总高度。scrollWidth:在没有滚动条的情况下,元素内容的总宽度。scrollLeft:被隐藏在内容区域左侧的像素数。通过设置该属性可以改变元素的滚动位置。scrollTop:被隐藏在内容区域上方的像素数。通过设置该属性可以改变元素的滚动位置。 4.确定元素大小getBoundingClientRect()方法,返回矩形对象,包含4个属性:left、top、right和bottom。(跨浏览器的getBoundingClientRect()方法)12345678910111213141516171819202122232425262728293031323334function getBoundingClientRect(element) { var scrollTop = document.documentElement.scrollTop; var scrollLeft = document.documentElement.scrollLeft; if (element.getBoundingClientRect) { if (typeof arguments.callee.offset != "number") { var temp = document.createElement("div"); temp.style.cssText = "position:absolute;left:0;top:0;"; document.body.appendChild(temp); arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop; document.body.removeChild(temp); temp = null; } var rect = element.getBoundingClientRect(); var offset = arguments.callee.offset; return { left: rect.left + offset, right: rect.right + offset, top: rect.top + offset, bottom: rect.bottom + offset }; } else { var actualLeft = getElementLeft(element); var actualTop = getElementTop(element); return { left: actualLeft - scrollLeft, right: actualLeft + element.offsetWidth - scrollLeft, top: actualTop - scrollTop, bottom: actualTop + element.offsetHeight - scrollTop } }} 遍历“DOM2级遍历和范围”模块定义了两个用于辅助完成顺序遍历DOM结构的类型:NodeIterator和TreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优先的遍历操作。 NodeIterator使用document.createNodeIterator()方法创建实例,这个方法接收4个参数:root:想要作为搜索起点的树中的节点。whatToShow:表示要访问哪些节点的数字代码。filter:是一个NodeFilter对象,或者表示应该接受还是拒绝某种特定节点的函数。entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在HTML页面没有用,因为其中的实体引用不能扩展。 NodeIterator的两个主要方法:nextNode()和previousNode(),两者遍历到DOM子树最后一个节点时,都返回null。12345678<div id="div1"> <p><b>Hello</b>world!<p> <ul> <li>List item 1</li> <li>List item 2</li> <li>List item 3</li> </ul></div> 遍历<div>中的所有元素:1234567var div = document.getElementById("div1");var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false);var node = iterator.nextNode();while(node!=null){ alert(node.tagName); node = iterator.nextNode;} 如果只想返回遍历中遇到的<li>,那么只要使用一个过滤器即可:var div = document.getElementById(“div1”);var filter = function(node){ return node.tagName.toLowerCase()==”li”? NodeFilter.FILTER_ACCEPT: NodeFilter.FILTER_SKIP;}; var iterator = document.createNodeIterator(div,NodeFilter.SHOW_ELEMENT,filter,false); var node = iterator.nextNode();while(node!=null){ alert(node.tagName); node = iterator.nextNode;} TreeWalkerTreeWalker是NodeIterator的一个更高级版本。使用document.createTreeWalker()方法创建,接收4个与document.createNodeIterator()方法相同的参数。除了包括nextNode()和previousNode()在内的相同的功能之外,这个类型还提供了下列用于在不同方向上遍历DOM结构的方法:parentNode():遍历到当前节点的父节点。firstChild():遍历到当前节点的第一个子节点。lastChild():遍历到当前节点的最后一个子节点。nextSibling():遍历到当前节点的下一个兄弟节点。previousSibling():遍历到当前节点的上一个兄弟节点。1234567891011121314var div =document.getElementById("div1");var filter = function(node){ return node.tagName.toLowerCase()=="li"? NodeFilter.FILTER_ACCEPT: NodeFilter.FILTER_SKIP; };var walker = document.createTreeWalker(div,NodeFilter.SHOW_ELEMENT,filter,false);var node = walker .nextNode();while(node!=null){ alert(node.tagName); node = walker .nextNode;} TreeWalker还有一个属性,名叫currentNode,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性也可以修改遍历继续进行的起点:123var node = walker.nextNode();alert(node === walker.currentNode); //truewalker.currentNode = document.body; //修改起点 范围DOM2级在Document类型中定义了createRange()方法,让开发人员更方便地控制页面。每个范围由一个Range类型的实例表示,该实例拥有以下的属性:startContainer:包含范围起点的节点。startOffset:范围在startContainer中起点的偏移量。endContainer:包含范围终点的节点。endOffset:范围在endContainer中终点的偏移量。commonAncestorContainer:startContainer和endContainer共同的祖先节点在文档树中位置最深的那个。1.用DOM范围实现简单选择使用selectNode()和selectNodeContents()。这两个方法都接收衣蛾参数,即一个DOM节点,然后使用该节点中的信息来填充范围。selectNode()方法选择整个节点,包括其子节点;而selectNodeContents()方法则只选择节点的子节点。2.用DOM范围实现复杂选择使用setStart()和setEnd()方法。这两个方法都接受两个参数:一个参照节点和一个偏移量值。12345678910<p id="p1"><b>Hello</b>world!</p>var p1=document.getElementById("p1");var helloNode=p1.firstChild.firstChild, worldNode=p1.lastChild;var range = document.createRange();range.setStart(helloNode,2);range.setEnd(worldNode,3);红色部分则为已选择的范围:<p id="p1"><b>Hello</b>world!</p> 3.操作DOM范围中的内容deleteContents():从文档中删除范围所包含的内容。12345678910var p1 = document.getElementById("p1");var helloNode = p1.firstChild.firstChild, worldNode = p1.lastChild;var range = document.createRange();range.setStart(helloNode, 2);range.setEnd(worldNode, 3);range.deleteContents();// 结果为:<p id="p1"><b>He</b>rld!</p> 由于范围选区在修改底层DOM结构时能够保证格式良好,因此即使内容被删除了,最终的DOM结构依旧是格式良好的。与deleteContents()相似,extractContents()也会从文档中移除范围选区。区别在于extractContents()会返回范围的文档片段。 cloneContents()创建范围对象的一个副本,然后再文档的其他地方插入该副本。4.插入DOM范围中的内容insertNode()向范围选区的开始处插入一个节点。12345678910111213141516<span style="color : red">Inserted Text</span>var p1=document.getElementById("p1");var helloNode=p1.firstChild.firstChild, worldNode=p1.lastChild;var range = document.createRange();range.setStart(helloNode,2);range.setEnd(worldNode,3);var span = document.createElement("span");span.style.color="red";span.appendChild(document.createTextNode("Inserted text"));range.insertNode(span);// 结果为:<p id="p1"><b>He<span style="color : red">Inserted Text</span>llo</b>world!</p> 除了向范围内部插入内容之外,还可以环绕范围插入内容,使用surroundContents()方法。这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台执行下列步骤:1)提取出范围中的内容2)将给定节点插入到文档中原来范围所在的位置上3)将文档片段的内容添加到给定节点中。123456789101112var p1=document.getElementById("p1");var helloNode=p1.firstChild.firstChild, worldNode=p1.lastChild;var range = document.createRange();range.selectNode(helloNode);var span = document.createElement("span");span.style.backgroundColor="yellow";range.surroundContents(span);// 结果为:<p id="p1"><b><span style="background-color : yellow">Hello</span></b>world!</p> 5.折叠DOM范围所谓折叠范围,就是指范围中未选择文档的任何部分。使用collapse()方法来折叠范围,这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。true表示折叠到范围的起点,false表示折叠到范围的终点。也可以通过检测某个范围是否处于折叠状态,来确定范围中的两个节点是否紧密相邻。通过range.collapse判断6.比较DOM范围使用compareBoundaryPoints()方法确定多个范围是否有公共的边界(起点或终点)。该方法接受两个参数:表示比较方式的常量值和要比较的范围。返回值如下:如果第一个范围中的点位于第二个范围中的点之前,返回-1;如果两个点相等,返回0;如果第一个范围中的点位于第二个范围中的点之后,返回1。7.复制DOM范围cloneRange()方法1var newrange = range.cloneRange(); 8.清理DOM范围detach()方法12range.detach();range=null;]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(三)]]></title>
<url>%2F2017%2F05%2F13%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89%2F</url>
<content type="text"><![CDATA[BOM(浏览器对象模型)window对象BOM的核心对象是window,它表示浏览器的一个实例。在浏览器中,window对象有双重角色,它既是通过Javascript访问浏览器窗口的一个接口,又是ECMAScript规定的Global对象。这意味着网页中定义的任何一个对象、变量和函数,都以window作为其Global对象,因此有权访问parseInt()等方法。 定义全局变量和在window对象上直接定义属性还是有一点差别:全局变量不能通过delete操作符删除,而直接在window对象上定义的属性可以(IE9以下会报错)尝试访问未声明的变量会抛出错误,但是通过查询window对象,可以知道某个可能未声明的变量是否存在。 窗口位置使用下列代码可以跨浏览器去得窗口左边和上边的位置:1234var leftPos = (typeof window.screenLeft == "number") ? window.screenLeft : window.screenX;var topPos = (typeof window.screenTop == "number") ? window.screenTop : window.screenY; 使用moveTo()和moveBy()方法有可能将窗口精确地移动到一个新位置。这两个方法都接收两个参数,moveTo()接收的是新位置的x和y坐标值,而moveBy()接收的是水平和垂直方向上移动的像素数。eg:window.moveTo(0,0); //移往屏幕左上角window.moveBy(0,100); //将窗口向下移动100像素 这两种方法可能被浏览器禁用,且都不适用于框架,只能对window对象使用。 窗口大小窗口的属性:innerWidth、innerHeight、outerWidth、outerHeight;outerWidth和outerHeight返回浏览器窗口本身的尺寸。但在Opera中,这两个属性的值表示页面视图容器的大小。innerWidth和innerHeight则表示该容器中页面视图区的大小。在Chrome中,innerWidth、innerHeight、outerWidth、outerHeight返回相同的值,即视口大小而非浏览器窗口大小。 此外还可以使用DOM方法取得页面可见区域的相关信息:标准模式下:document.documentElement.clientWidth 和 document.documentElement.clientHeight混杂模式下:document.body.clientWidth 和 document.body.clientHeight。123456789101112var pageWidth = window.innerWidth, pageHeight = window.innerHeight;if(typeof pageWidth != "number") { if(document.compatMode == "CSS1Compat") { //判断页面是否处于标准模式 pageWidth = document.documentElement.clientWidth; pageHeight = document.documentElement.clientHeight; }else{ pageWidth = document.body.clientWidth; pageHeight = document.body.clientHeight; }} 调整浏览器窗口大小:resizeTo()和resizeBy()resizeTo()接收浏览器窗口的新宽度和新高度,而resizeBy()接收新窗口与原窗口的宽度和高度之差。1234//调整到100*100window.resizeTo(100,100);//调整到200*150window.resizeBy(100,50); 导航和打开窗口window.open()方法既可以导航到一个特定的URL,也可以打开一个新窗口。接收四个参数:要加载的URL、窗口目标、一个特性子字符串以及一个新页面是否取代浏览器历史记录中当前加载页面的布尔值。12window.open("http://www.wrox.com","wroxWindow","height=400,width=400,resizable=yes");// 返回一个指向新窗口的引用,引用的对象与window对象大致相似。 可以调用close()方法关闭新打开的窗口,仅适用于通过window.open()打开的弹出窗口。对于浏览器的主窗口,如果没有用户的允许是不能关 间歇调用和超时调用javascript是单线程语言,但它允许通过设置超时值和间歇时间值来调度代码在特定的时刻执行。前者是在指定的时间过后执行代码,而后者则是每隔指定的时间就执行一次代码。 超时调用使用window对象的setTimeout(),接收两个参数;setTimeout()返回一个数值ID,表示超时调用。这个ID是执行代码的唯一标识符,可以通过它来取消超时调用。1234var timeid=setTimeout(function() { alert("hi");},1000);clearTimeout(timeid); 间歇调用使用window对象的setInterval(),接收参数与setTimeout()相同,也返回一个间歇调用ID,使用clearInterval()方法取消间歇调用。 超时调用setTimeout()和间歇调用setInterval()的区别是:setTimeout()是指在隔多久后就执行代码,仅仅执行一次;setInterval()是指每隔多久就执行代码一次,执行次数不定。 一般认为,使用超时调用来模拟间歇调用的是一种最佳模式。最好不要使用间歇调用。在开发环境下,很少使用真正的间歇调用,原因是后一个间歇调用可能会在前一个间歇调用结束之前启动。 系统对话框alert()、confirm()和prompt();javascript中可以打开的对话框:查找对话框:window.find();打印对话框:window.print() location对象BOM最有用的对象之一,同时它也是window对象是属性,也是document对象的属性。查询字符串参数:12345678910111213141516171819function getQueryStringArgs() { //取得查询字符串并去掉开头的问号 var qs = (location.search.length > 0 ? location.search.substring(1) : ""); var args = {}, items = qs.length ? qs.split("&") : [], item = null, name = null, value = null; var len = items.length; for(var i = 0; i < len; i++) { item = items[i].split("="); name = decodeURIComponent(item[0]); value = decodeURIComponent(item[1]); if(name.length) { args[name] = value; } } return args;} 位置操作:立即打开新URL并在浏览器的历史记录中生成一条记录。12345678910111213141516171819202122location.assign("http://www.wrox.com");// 同等效果的代码:window.location = "http://www.wrox.com";location.href ="http://www.wrox.com"// location对象的其他属性:// 假设初始URL为http://www.wrox.com/WileyCDA/// 将URL修改为http://www.wrox.com/WileyCDA/#section1location.hash = "#section1";// 将URL修改为http://www.wrox.com/WileyCDA/?q=javascriptlocation.search = "?q=javascript";// 将URL修改为http://www.yahoo.com/WileyCDA/location.hostname = "www.yahoo.com";// 将URL修改为http://www.wrox.com/mydir/location.pathname = "mydir";// 将URL修改为http://www.wrox.com:8080/WileyCDA/location.port=8080; 每次修改location的属性(hash除外),页面都会以新URL重新加载而调用replace方法,可以在修改URL之后,用户不可通过单击“后退”按钮导航到前一个页面。location.replace(“http://www.wrox.com“);最后一个是reload()方法,作用是重新加载当前显示的页面。 navigator对象navigator对象的属性通常是用来检测显示网页的浏览器类型检测插件:使用plugins数组(包含的属性:name,description,filename,length)1234567891011121314151617181920212223//IE中无效function hasPlugin(name) { name = name.toLowerCase(); for(var i = 0; i < navigator.plugins.length; i++) { if(navigator.plugins[i].name.toLowerCase().indexOf(name) > -1) { return true; } } return false;}//检测Flashalert(hasPlugin("Flash"));//检测IE中的插件function hasPlugin(name){ try{ new ActiveXObject(name); return true; }catch(ex){ return false }}alert(hasPlugin("ShockwaveFlash.ShockwaveFlash")); screen对象在javascript编程中用处不大,略过 history对象history是window对象的属性,因此每个浏览器窗口、每个标签页乃至每个框架,都有自己的history对象与特定的window对象关联。使用go()方法可以在用户的历史记录中任意跳转。123456//后退一页history.go(-1);//前进一页history.go(1);//前进两页history.go(2); 也可以给go()方法传递一个字符串参数,此时浏览器会跳转到历史记录中包含该字符串的第一个位置。还有两个简写方法back()和forward()方法来代替go()的后退和前进此外,还可以通过检查history.length是否等于0,来检测当前页面是不是用户历史记录中的第一个页面。 客户端检测各个浏览器之间存在着差异,而客户端检测则是对此的一种补救措施,也是一种行之有效的开发策略。最常用也最为人们广泛接受的客户端检测形式是能力检测(又称特性检测)。能力检测的目标不是识别特定的浏览器,而是识别浏览器的能力。基本模式:123if(object.propertyInQuestion){ //使用object.propertyInQuestion} 能力检测的两个重要概念:1.先检测达成目的的最常用的特性;2.必须检测试剂要用到的特性 更可靠的能力检测检测某方法是不是一个函数。如sort:123function isSortable(object){ return typeof object.sort == "function";} 尽量使用typeof进行能力检测IE8及以前版本中使用typeof document.createElement返回的是“object”,IE9已修复。在浏览器环境下测试任何对象的某个特性是否存在,要使用下面这个函数。12345function isHostMethod(object,property){ var t = typeof object[property]; return t == "function" || (!!(t=='object'&&object[property])) || t=="unknown";}result = isHostMethod(xhr,"open"); 能力检测不是浏览器检测在开发中应该将能力检测作为确定下一步解决方案的依据,而不是用它来判断用户使用的是什么浏览器 怪癖检测是为了识别浏览器的特殊行为,主要是想知道浏览器存在什么缺陷。 用户代理检测争议最大的一种客户端检测技术,通过检测用户代理字符串来确定实际使用的浏览器。在每一次HTTP请求过程中,用户代理字符串是作为响应首部发送的,可以通过navigator.userAgent访问在服务器端常用用户代理检测,在客户端则是被当作一种万不得已的做法。 用户代理字符串检测:完整代码:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167var client = function(){ //rendering engines var engine = { ie: 0, gecko: 0, webkit: 0, khtml: 0, opera: 0, //complete version ver: null }; //browsers var browser = { //browsers ie: 0, firefox: 0, safari: 0, konq: 0, opera: 0, chrome: 0, //specific version ver: null };//platform/device/OS var system = { win: false, mac: false, x11: false, //mobile devices iphone: false, ipod: false, ipad: false, ios: false, android: false, nokiaN: false, winMobile: false, //game systems wii: false, ps: false }; //detect rendering engines/browsers var ua = navigator.userAgent; if (window.opera) { engine.ver = browser.ver = window.opera.version(); engine.opera = browser.opera = parseFloat(engine.ver); } else if (/AppleWebKit\/(\S+)/.test(ua)){ engine.ver = RegExp["$1"]; engine.webkit = parseFloat(engine.ver); //figure out if it's Chrome or Safari if (/Chrome\/(\S+)/.test(ua)){ browser.ver = RegExp["$1"]; browser.chrome = parseFloat(browser.ver); } else if (/Version\/(\S+)/.test(ua)){ browser.ver = RegExp["$1"]; browser.safari = parseFloat(browser.ver); } else { //approximate version var safariVersion = 1; if (engine.webkit < 100){ safariVersion = 1; } else if (engine.webkit < 312){ safariVersion = 1.2; } else if (engine.webkit < 412){ safariVersion = 1.3; } else { safariVersion = 2; } browser.safari = browser.ver = safariVersion; } } else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)){ engine.ver = browser.ver = RegExp["$1"]; engine.khtml = browser.konq = parseFloat(engine.ver); } else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)){ engine.ver = RegExp["$1"]; engine.gecko = parseFloat(engine.ver); //determine if it's Firefox if (/Firefox\/(\S+)/.test(ua)){ browser.ver = RegExp["$1"]; browser.firefox = parseFloat(browser.ver); } } else if (/MSIE ([^;]+)/.test(ua)){ engine.ver = browser.ver = RegExp["$1"]; engine.ie = browser.ie = parseFloat(engine.ver); } //detect browsers browser.ie = engine.ie; browser.opera = engine.opera; //detect platform var p = navigator.platform; system.win = p.indexOf("Win") == 0; system.mac = p.indexOf("Mac") == 0; system.x11 = (p == "X11") || (p.indexOf("Linux") == 0); //detect windows operating systems if (system.win){ if (/Win(?:dows )?([^do]{2})\s?(\d+\.\d+)?/.test(ua)){ if (RegExp["$1"] == "NT"){ switch(RegExp["$2"]){ case "5.0": system.win = "2000"; break; case "5.1": system.win = "XP"; break; case "6.0": system.win = "Vista"; break; case "6.1": system.win = "7"; break; default: system.win = "NT"; break; } } else if (RegExp["$1"] == "9x"){ system.win = "ME"; } else { system.win = RegExp["$1"]; } } } //mobile devices system.iphone = ua.indexOf("iPhone") > -1; system.ipod = ua.indexOf("iPod") > -1; system.ipad = ua.indexOf("iPad") > -1; system.nokiaN = ua.indexOf("NokiaN") > -1; //windows mobile if (system.win == "CE") { system.winMobile = system.win; } else if (system.win == "Ph"){ if(/Windows Phone OS (\d+.\d+)/.test(ua)){; system.win = "Phone"; system.winMobile = parseFloat(RegExp["$1"]); } } //determine iOS version if (system.mac && ua.indexOf("Mobile") > -1){ if (/CPU (?:iPhone )?OS (\d+_\d+)/.test(ua)){ system.ios = parseFloat(RegExp.$1.replace("_", ".")); } else { system.ios = 2; //can't really detect - so guess } } //determine Android version if (/Android (\d+\.\d+)/.test(ua)) { system.android = parseFloat(RegExp.$1); } //gaming systems system.wii = ua.indexOf("Wii") > -1; system.ps = /playstation/i.test(ua); //return it return { engine: engine, browser: browser, system: system };}(); DOM文档对象模型DOM描绘了一个层次化的节点树,允许开发人员添加、删除和修改页面的某一部分。IE中DOM对象都是以COM对象的形式实现的。文档节点是每个文档的根节点。在HTML页面中,<html>是文档节点的唯一子节点,称之为文档元素。文档元素是文档的最外层元素,文档的其他所有元素都包含在文档元素中。每个文档只有一个文档元素。 Node类型(IE不支持)Javascript中的所有节点类型都继承自Node类型,因此所有节点类型都共享着相同的基本属性和方法。每个节点都有一个nodeType属性:最常用:如果节点是元素节点,则 nodeType 属性将返回 1。如果节点是属性节点,则 nodeType 属性将返回 2。 nodeName和nodeValue 节点关系文档中所有的节点之间都存在这样或那样的关系。每个节点都有childNodes属性,其中保存着一个NodeList对象,这是个类数组对象(不是Array的实例),用于保存一组有序的节点。NodeList的独特之处在于,它实际上是基于DOM结构动态执行查询的结果,因此DOM结构的变化能够自动反映在NodeList中。123var firstChild = someNode.childNodes[0];var secondChild = someNode.childNodes.item(1);var count = someNode.childNodes.length; 将NodeList对象转换为数组:12//IE8及之前版本无效var arrayOfNodes = Array.prototype.slice.call(some.childNodes,0); 将NodeList对象转换为数组,适用于所有浏览器中123456789101112function convertToArray(nodes) { var array = null; try{ array = Array.prototype.slice.call(nodes,0); }catch(ex) { array = new Array(); for(var i = 0; i < nodes.length; i++) { array.push(nodes[i]); } } return array;} 每个节点都有一个parentNode属性,该属性指向文档树中的父节点。兄弟节点:previousSibling和nextSibling第一个节点和最后一个节点:firstChild和lastChild最后还有一个属性是ownerDocument,该属性指向表示整个文档的文档节点。 操作节点添加节点和插入节点:appendChild()和insertBefore()12var returnNode = someNode.appendChild(someNode.firstChild);var returnNode = someNode.insertBefore(newNode,null); 自定义实现的insertAfter()函数12345678function insertAfter(newElement, targetElement) { var parent = targetElement.parentNode; if (parent.lastChild == targetElement) { parent.appendChild(newElement); }else{ parent.insertBefore(newElement, targetElement.nextSibling); }} 接收两个参数,需要插入的节点和目标节点(在该节点后插入) replaceNode()替换节点,接收两个参数:要插入的节点和要替换的节点。cloneNode()克隆节点,用于创建调用这个方法的节点的一个完全相同的副本。只接收一个布尔值参数,表示是否执行深复制。在参数为true时,执行深复制,也就是复制节点及其整个子节点树。normalize(),唯一的作用是处理文档树中的文本节点。当某个节点调用这个方法时,就会在该节点的后代节点中查找是否有空文本节点或连续两个文本节点,如果有空文本节点,则删除;如果找到相邻的文本节点,则将他们合并为一个文本节点。 Document类型在浏览器中,document对象是HTMLDocument(继承自Document类型)的一个实例,表示整个HTML页面。此外,document对象是window对象的一个属性,因此可以将其作为全局对象来访问。1.文档的子节点内置的访问其子节点的快捷方式:document.documentElement属性和childNode列表。body属性,document.body直接指向<body>元素。 通过document.doctype获取<!DOCTYPE>的引用(各个浏览器的支持不一致,因此这个属性的用处很有限) 2.文档信息获取文档标题:document.title;设置文档标题:document.title = “New page title”获取URL:document.URL获取域名:document.domain获取来源页面的URL:document.referrerURL和domain属性是相互关联的。eg:URL是http://www.wrox.com/WileyCDA/,那么域名domain则是www.wrox.com这三个属性,只有域名domain可以设置。可以通过设置document.domain来实现js跨域通信。浏览器对domain属性有一个限制,即如果域名一开始是”松散的“,那么不能将它再设置为”紧绷的“。换句话说,原来的document.domain为”p2p.wrox.com“,在将document.domain设置为”wrox.com“之后,就不能再将其设置回原来的域名”p2p.wrox.com“。 3.查找元素document.getElementById()和document.getElementsByTagName()如果有多个相同的id,document.getElementById()则只返回文档中第一次出现的元素。IE7及较低版本,不区分ID大小写,并且document.getElementById()中传入的是某个元素的name属性的值,也会返回相应元素。123document.getElementsByName(),这个方法会返回带有特定name特性的所有元素。最常用在获取单选按钮中。document.getElementByClassName()(通过类名获取元素,只支持在较高级的浏览器中,且不完全支持,慎用) 4.特殊集合document.anchors,包含文档中所有带name特性的<a>元素;document.applets,包含文档中所有的<applets>元素;document.forms,包含文档中所有d<form>元素;document.images,包含文档中所有d<img>元素;document.links,包含文档中所有带href特性的<a>元素; 5.DOM一致性检测(应该在能力检测完成后进行)DOM分为多个级别,也包含多个部分,因此检测浏览器实现了DOM的哪些部分就十分必要了。document.implementation属性就是为此提供相应信息和功能的对象,与浏览器对DOM的实现直接对应。DOM1级的document.implementation只有一个方法,hasFeature()。该方法只接受两个参数:要检测的DOM功能的名称及版本号。如果浏览器支持给定名称和版本号,则返回true. 6.文档写入document.write()、document.writeln()、document.open()、document.close() Element类型1.HTML元素属性:id,title,lang,dir(语言方向),className 2.取得特性getAttribute();特别的,获取元素的类名是getAttribute(“class”),而不是getAttribute(“className”)。有两类特殊的特性:style和类似onclick这样的事件处理程序通过getAttribute()获取的style,返回的是CSS文本,而通过属性访问它则返回一个对象。通过getAttribute()获取的onclick,返回的是相应的js代码字符串,而通过属性访问它则返回一个javascript函数。 3.设置特性setAttribute();如果设置的特性不存在,setAttribute()则创建该属性并设置相应的值。 4.清除特性removeAttribute(); 5.attributes属性Element类型是使用attributes属性的唯一一个DOM节点类型。attributes属性包含一个NameNodeMap,与NodeList类似。NameNodeMap拥有下列方法:getNamedItem(name):返回nodeName属性等于name的节点;removeNamedItem(name):从列表中删除nodeName属性等于name的节点;setNamedItem(node):向列表添加节点,以节点的nodeName属性为索引;item(pos):返回位于数字pos位置处的节点。12var id = element.attributes.getNamedItem("id").nodeValue;var id = element.attributes["id"].nodeValue; 6.创建元素12document.createElement()var div =document.createElement("<div id=\"myNewDiv\" class=\"box\"></div>"); 7.元素的子节点元素可以有任意数目的子节点和后代节点。获取子节点时通常要判断节点类型,进行相应的操作。12345for(var i=0,len=element.childNodes.length;i<len;i++){ if(element.childNodes[i].nodeType==1){ //dosomething }} Text类型nodeType=3,不支持子节点;appendData(text):将text添加到节点末尾deleteData(offset,count):从offset指定的位置开始删除count个字符insertData(offset,text):从offset指定位置插入textreplaceData(offset,count,text):用text替换从offset指定的位置开始到offset+count为止处的文本splitData(offset):从offset指定的位置将当前文本节点分成两个文本节点。substringData(offset,count):提取从offset指定的位置开始到offset+count为止处的字符串空格和换行也算一个文本节点1.创建文本节点document.createTextNode(); 2.规范化文本节点使用normalize()方法,将空文本节点删除,将连续的两个文本节点合并 3.分割文本节点使用splitText(),将一个文本节点分成两个文本节点,与normalize相反 Comment类型DOM中的注释类型,与Text类型继承自相同的基类,因此它拥有除splitText()之外的所有字符串操作方法可以通过nodeValue或data属性来获取注释的内容1234<div id="mydiv"><!--A comment--></div>var div = documemt.getElementById("mydiv");var comment = div.firstChild;alert(comment.data); //A comment CDATASection类型只针对于XML的文档,表示的是CDATA区域 DocumentType类型仅有firefox、Safari和Opera支持,包含着与文档的doctype相关的信息。没有子节点。 DocumentFragment类型所有节点类型中,只有DocumentFragment在文档中没有对应的标记。12345678910var fragment = document.createDocumentFragment();var ul = document.getElementById("mylist");var li = null;for(var i = 0; i < 3; i++){ li = document.createElement("li"); li.appendChild(document.createTextNode("Item "+(i+1))); fragment.appendChild(li);}ul.appendChild(fragment); Attr类型元素的特性在DOM中已Attr类型来表示。特性就是存在于attributes属性中的节点。Attr有3个属性:name、value和specified(布尔值,用以区别特性是在代码中指定的,还是默认的)。有下列方法(不常用):createAttribute()、setAttributeNode()和getAttributeNode()]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(二)]]></title>
<url>%2F2017%2F05%2F12%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89%2F</url>
<content type="text"><![CDATA[面向对象的JavaScriptECMAScript中有两种属性:数据属性和访问器属性数据属性:configurable、enumerable、writable、value只能使用Object.defineProperty()方法才可以修改属性默认的特性。该方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象可以多次调用Object.defineProperty()方法修改同一属性,但在把configurable特性设置为false后就会有限制了。 访问器属性:configurable、enumerable、get、set变量名以下划线开头(_year),用于表示只能通过对象方法访问的属性。Object.defineProperties()可以一次定义多个属性。1234567891011121314151617181920var book={};Object.defineProperties(book, { _year: { value: 2004 }, edition: { value: 1 }, year: { get: function(){ return this._year; }, set: function(newValue){ if(newValue > 2004){ this._year = newValue; this.edition += newValue-2004; } } }}); Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符,接收两个参数:属性所在对象和要读取其描述符的属性名称1var descriptor=Object.getOwnPropertyDescriptor(book,"_year"); 创建对象工厂模式:抽象了创建具体对象的过程。(没有解决对象识别问题)1234567891011function createPerson(name,age,job){ var o = new Object(); o.name=name; o.age=age; o.job=job; o.sayName=function(){ alert(this.name); } return o;}var person=createPerson("Peter",22,"Student"); 构造函数模式:创建自定义的构造函数,从而定义自定义对象类型的属性和方法function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.sayName=function(){ alert(this.name); }}var person=new Person(“Peter”,22,”Student”); 使用new操作符调用构造函数会经历4个步骤:(1)创建一个新对象,分配堆内存;(2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象);(3)执行构造函数中的代码(为这个新对象添加属性);(4)返回新对象。 构造函数的缺点:每个方法都要在每个实例上重新创建一遍。解决方法:将构造函数中的方法转移到构造函数外部,成为全局的函数方法。 原型模式:每个函数都有prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的的属性和方法。123456789function Person(){}Person.prototype.name = "Peter";Person.prototype.age = 22;Person.prototype.job = "Student";Person.prototype.sayName = function(){ alert(this.name);}var person =new Person();//Person.prototype.constructor指向Person. ECMAScript 5 中增加一个新方法:Object.getPrototypeOf(),返回对象的原型。不能通过对象实例重写原型中的值。当为对象实例添加属性时,这个属性会屏蔽原型对象中保存的同名属性 使用hasOwnPrototype()方法可以检测一个属性时存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例中时,才会返回true. in操作符:会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。(常用在for-in结构中)1234//判断属性是在对象实例中,还是在原型中function hasPrototypeProperty(object,name){ return !object.hasOwnProperty(name) && (name in object);} 要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5中的Oject.keys()方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。若想得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()。实例与原型之间是松散连接关系。实例中的指针仅指向原型,而不指向构造函数。 不推荐直接修改原生对象的原型,如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能导致命名冲突。而且,这样做也可能会意外地重写原生方法。 原型对象的问题:因为所有实例都共享原型中的所有属性,所有无法做到实例的属性私有化。 组合使用构造函数模式和原型模式(使用最广泛的)创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定于方法和共享的属性。12345678910111213function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.friends=["Shelby","Court"];}Person.prototype={ constructor : Person; sayName : function(){ alert(this.name); }} 动态原型模式把所有信息都封装在构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。1234567891011function Person(name,age,job){ this.name=name; this.age=age; this.job=job;//检测是否存在sayName()方法 if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); } }} 使用动态原型模式时,不能使用对象字面量重写原型。 寄生构造函数模式创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。1234567891011function Person(name,age,job){ var o = new Object(); o.name=name; o.age=age; o.job=job; o.sayName=function(){ alert(this.name); }; return o;}var person = new Person("Peter",22,"Student"); 这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,可以使用这个模式:12345678910function SpecialArray(){ var values = new Array(); values.push.apply(values,arguments); values.toPipedString = function(){ return this.join("|"); } return values;}var colors = new SpecialArray("red","green","blue");alert(colors.toPipedString()); 该模式下,返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说 ,构造函数返回的对象与在构造函数外部创建的对象没什么不同。 稳妥构造函数模式稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥构造函数与寄生构造函数类似,但有两点不同:一是新创建的对象的实例方法不引用this;二是不适用new操作符调用构造函数。12345678910function Person(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName=function(){ alert(name); }; return o;} 继承1.使用原型链实现继承基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法(即让一个引用类型的原型指向了另一个引用类型)。构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。 实现原型链的基本模式:123456789101112131415161718192021function SuperType(){ this.property = true;}SuperType.prototype.getSuperValue = function(){ return this.property;};function SubType(){ this.subproperty = false;}//继承了SuperTypeSubType.prototype = new SuperType();SubType.prototype.getSubValue = function(){ return this.subproperty;};var instance = new SubType();alert(instance.getSuperValue()); //true 实现的本质:重写原型对象,代之以一个新类型的实例。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因。 确定原型和实例的关系:instanceof和isPrototypeOf(); 通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。 使用对象字面量创建的实例是object的实例。使用原型链实现继承的问题:(1)、所有原型链上的原型都共享属性;(2)、在创建子类型的实例时,不能向超类型的构造函数中传递参数。 2.借用构造函数基本思想:在子类型构造函数的内部调用超类型构造函数。主要是使用apply(),call()。12345678function SuperType(){ this.colors=["red","blue","green"];}function SubType(){ //继承了SuperType SuperType.call(this);} 借用构造函数有个优势,即可以在子类型构造函数中向超类型构造函数传递参数。 3.组合继承(也叫伪经典继承)最常用的继承模式指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。基本思路:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。1234567891011121314151617181920212223242526function SuperType(name){ this.name = name; this.colors = ["red","blue","green"];}SuperType.prototype.sayName = function(){ alert(this.name);}function SubType(name,age){ //继承属性 SuperType.call(this,name); this.age = age;}//继承方法SubType.prototype = new SuperType();SubType.prototype.constructor = SubType;SubType.prototype.sayAge = function(){ alert(this.age);};var instance = new SubType("Peter",22);instance.colors.push("black");alert(instance.colors); // "red","blue","green","black"instance.sayName(); // Peterinstance.sayAge(); // 22 4.原型式继承借助原型可以基于已有的对象创建对象,同时还不必因此创建自定义类型。12345function object(o){ function F(){} F.prototype = o; return new F();} 在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。就是将一个对象传入object()函数,然后根据具体需求对得到的对象加以修改。 ECMAScript 5定义了Object.create()来规范原型式继承。这个方法接收两个参数:用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()和上面的object()方法的行为相同。1234567891011121314var Person = { name : "Peter"; friends : ["Shelby","Court","Van"];};var person1 = Object.create(Person);person1.name = "Greg";person1.friends.push("Rob");var person2 = Object.create(Person);person2.name = "Linda";person2.friends.push("Barbie");alert(Person.friends); //"Shelby","Court","Van","Rob","Barbie" 5.寄生式继承(类似于工厂模式)创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后在想真地是它做了所有工作一样返回对象。1234567function createAnother(original){ var clone = object(original); //这里的object()函数就是前面提到的原型式继承的方法 clone.sayHi=function(){ //以某种方式来增强这个对象 alert("hi"); } return clone; //返回这个对象} 寄生式继承和构造函数模式类似,都会由于不能做到函数复用而降低效率。 6.寄生组合式继承(最理想的继承范式)通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。减少了一次调用SuperType构造函数。基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。基本模式如下:12345function inheritPrototype(subType,superType){ var prototype = Object.create(superType.prototype); //创建对象 prototype.constructor = subType; //增强对象 subType.prototype = prototype; //指定对象} 在函数内部,先是创建超类型原型的一个副本,然后为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性,最后,将新创建的对象(即副本)赋值给子类型的原型。12345678910111213141516171819function SuperType(name){ this.name = name; this.colors = ["red","blue","green"];}SuperType.prototype.sayName = function(){ alert(this.name);}function SubType(name,age){ SuperType.call(this,name); this.age = age;}inheritPrototype(SubType, SuperType);SubType.prototype.sayAge = function(){ alert(this.age);}; 函数表达式定义函数的两种方式:一种是函数声明,另一种是函数表达式;函数声明:function foo(){}函数表达式:var foo = function(){}(函数表达式就是把函数当作值来使用) 递归函数:可以使用arguments.callee属性来实现:1234567function factorial(num) { if(num <= 1){ return 1; }else{ return num * arguments.callee(num-1); //arguments.callee指向的是factorial() }} 但在严格模式下,使用arguments.callee会导致错误,所以更好的递归实现是使用命名函数表达式:1234567var factorial = (function f(num) { if(num <= 1) { return 1; }else { return num * f(num - 1); }}); 闭包闭包:是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式:在一个函数内部创建另一个函数作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。一般的,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域,但闭包的情况有所不同。 在一个函数内部定义的函数(内部函数)会将包含函数(外部函数)的活动对象添加到它的作用域链中。当外部函数执行完毕后,其活动对象不会被销毁,因为内部匿名函数的作用域链仍然在引用这个活动对象。 由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。由于作用域链的配置机制,所以闭包只能取得包含函数中任何变量的最后一个值。修改前:1234567891011function createFun() { var result = new Array(); for(var i = 0; i < 10;i++) { result[i] = function() { return i; }; } return result;}var arr=createFun(); //返回函数数组console.log(arr[0]()); //执行保存在函数数组中的第一个函数,返回10 可以通过创建另一个匿名函数强制让闭包的行为符合预期:修改后:12345678910111213function createFun(){ var result = new Array(); for(var i = 0; i < 10;i++) { result[i] = function(num) { return function() { return num; }; }(i); } return result;}var arr=createFun(); //返回函数数组console.log(arr[0]()); //执行保存在函数数组中的第一个函数,返回0 以上是《javascript高级程序设计 第3版》181页中的内容,大概是说修改前每个函数引用的是同一个变量i,所以返回的变量i是10;而修改后使变量i传递到一个匿名函数里,然后通过创建闭包返回,所以函数数组中的每个函数都能返回各自的值。若是想让result数组保存匿名函数返回的各自的值,那么代码如下:12345678910function createFun(){ var result=new Array(); for(var i = 0; i<10;i++) { result[i]=(function() { return i; })(); //让匿名函数立即执行 } return result;}console.log(createFun()); //返回[0,1,2,3,4,5,6,7,8,9] 关于this对象匿名函数的执行环境具有全局性,因此其this对象通常指向window。12345678910var name = "The Window";var object = { name : "My Object", getNameFunc : function() { return function() { //返回匿名函数 return this.name; }; }};alert(object.getNameFunc()()); //“The Window” 每个函数在被调用时都会自动取得两个特殊变量:this和arguments,内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此无法直接访问外部函数中的这两个变量。可以通过将外部作用域中的this或arguments对象保存在一个闭包能够访问到的变量里:1234567891011var name="The Window";var object = { name: "My Object", getNameFunc: function() { var that = this; return function() { //返回匿名函数 return that.name; }; }};alert(object.getNameFunc()()); //"My Object" 意外改变this值的情况:12345678var name = "The Window";var object = { name: "My Object", getName: function() { return this.name; }}(object.getName = object.getName)(); 因为这个赋值表达式的值是函数本身,所以this的值不能得到维持,结果返回的是“The Window”. 内存泄漏如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁:123456function assignHandler(){ var element = document.getElementById("someElement"); element.onclick= function(){ alert(element.id); };} 以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包又创建了一个循环引用。element的引用至少是1,所以占用的内存不会被回收。修改:12345678function assignHandler(){ var element = document.getElementById("someElement"); var id=element.id; element.onclick= function() { alert(id); }; element = null;} 模仿块级作用域javascript没有块级作用域,javascript遇到多次声明的同一个变量,会对后续的声明视而不见。123456789function outputNumbers(count) { for(var i =0; i< count;i++) { alert(i); } var i; alert(i); //输出10;}var count = 10;outputNumbers(count); 使用匿名函数来模仿块级作用域,用作块级作用域(通常称为私有作用域)的匿名函数语法:123(function() { //块级作用域})() 以上代码定义并立即调用了匿名函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用,这是一个函数表达式。123456789function outputNumbers(count) { (function(){ //是闭包,实现了私有作用域 for(var i =0; i< count;i++) { alert(i); } })(); alert(i); //导致一个错误!} 私有变量在任何函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。如果在函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些私有变量。利用这一点,就可以创建用于访问私有变量的公有方法。 有权访问私有变量和私有函数的公有方法称为特权方法。创建特权方法的两种方式:一、在构造函数中定义特权方法123456789101112function Myobject() { var privateVariable = 10; function privateFunction() { return false; } //特权方法 this.publicMethod = function() { privateVariable++; return privateFunction(); }} 静态私有变量通过在私有作用域中定义私有变量或函数,也同样可以创建特权方法。12345678910111213141516(function(){ var privateVariable = 10; function privateFunction() { return false; } //构造函数 MyObject = function() {}; //特权方法 MyObject.prototype.publicMethod = function() { privateVariable++; return privateFunction(); };})(); 函数声明只能创建局部函数;初始化未经声明的变量,总是会创建一个全局变量。多查找作用域链中的一个层次,就会在一定程度上影响查找速度。 模块模式前面的模式是用于为自定义类型创建私有变量和特权方法的,而道格拉斯提出的模块模式则是为了单例创建私有变量和特权方法。 所谓单例,指的就是只有一个实例的对象,按照惯例,Javascript是以对象字面量的方式创建单例对象:123456var singleton = { name: value; method: function() { //code... }} 模块模式通过为单例添加私有变量和特权方法能够使其得到加强:123456789101112131415var singleton = { var privateVariable = 10; function privateFunction() { return false; } //特权方法和属性 return { publicProperty : true, publicMethod : function () { privateVariable++; return privateFunction(); } }}(); 这个模块模式使用了一个返回对象的匿名函数。由于对象是在匿名函数定义的,因此它的公有方法有权访问私有变量和函数。从本质上讲,这个对象字面量定义的是单例的公共接口。模块模式适用于在需要对单例进行某些初始化,同时又需要维护其私有变量时。12345678910111213141516171819var application = function() { var components = new Array(); //初始化 components.push(new BaseComponent()); //公共 return { getComponentCount: function() { return components.length; }, registerComponent: function () { if(typeof component == "object") { components.push(component); } } }; }(); 增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。123456789101112131415161718var singleton = { var privateVariable = 10; function privateFunction() { return false; } //创建某种类型的对象 var object = new CustonType(); //添加特权/公有属性和方法 object.publicProperty = true; object.publicMethod = function () { privateVariable++; return privateFunction(); }; //返回这个对象 return object;}();]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红宝书系列读书笔记(一)]]></title>
<url>%2F2017%2F05%2F11%2F%E7%AC%94%E8%AE%B0%2F%E7%BA%A2%E5%AE%9D%E4%B9%A6%E7%B3%BB%E5%88%97%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89%2F</url>
<content type="text"><![CDATA[前言:红宝书,即《JavaScript高级程序设计》一书,目前为第3版。这本书很合适新手入门,相比于《JavaScript权威指南》(犀牛书),它更容易帮助我们理解JavaScript,它从浅入深讲解了JavaScript,很多一开始难以理解的概念,都能通过这本书理解得更透彻。 基础概念JavaScript由三部分组成:ECMAScript、文档对象模型(DOM)、浏览器对象模型(BOM) DOM的核心规定的是如何映射基于XML的文档结构另外几种DOM标准:SVG、MathML、SMIL 文档模式:标准模式和混杂模式(诡异模式)IE6之前,浏览器渲染HTML文件的模式被称为诡异模式。 浏览器不支持JavaScript或js脚本被禁用时,使用<noscript> 标识符,就是指变量、函数、属性的名字,或者函数的参数。 ES5提出了“严格模式”,要在整个脚本中启用严格模式,可以在顶部添加:“use strict”。 ECMAScript 的变量是松散类型,所谓松散类型(弱类型)就是可以用来保存任何类型的数据。 var关键字明确地为函数变量设定作用域 JS有五种基本数据类型:undefined、null、boolean、Number和String;一种复杂数据类型(引用类型):Object; 基本数据类型和复杂数据类型在存储上有区别,基本数据类型存储于栈;复杂数据类型,在堆中存储,在栈中存储堆指针 检测变量的数据类型——typeof 使用typeof检测未声明和为初始化的变量都是返回undefinedtypeof 检测null值返回object (null==undefined)返回true 0除以0才返回NaN,正数除以0返回Infinity,负数除以0返回-InfinityisNaN()函数判断是否“不是数值” 数值转换的函数:Number(),parseInt(),parseFloat(),分别转换为数字、整型、浮点型 String类型字符字面量是指转义字符。转义字符当作一个字符长度。ECMAScript中,字符串是不可变的。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量。 转换为字符串的方式:toString()和转型函数string()[toString()返回的是字符串的副本] object类型Object的每个实例都具有下列属性和方法:constructor:保存着用于创建当前对象的函数,即构造函数;hasOwnProperty:用于检查给定的属性在当前对象实例中是否存在;isPrototypeOf(object):用于检查传入的对象是否传入对象的原型;propertyIsEnumberable(propertyName):用于检查给定的属性是否能够使用for-in语句来枚举;toLocaleString(),toString(),valueOf(); 操作符递增和递减:1234567891011121314151617181920var num1 = 2;var num2 = 20;var num3 = num1-- + num2; //等于22var num4 = num1+num2; //等于21var s1="2";var s2="z";var b=false;var f=1.1;var o={ valueOf:function(){ return -1; }};s1++; //3s2++; //NaNb++; //1f--; //0.100000000000009(浮点舍入错误所致)o--; //-2 位操作符按位非(~):本质是操作数的负值减一,123var num1=25;var num2= ~num1;alert(num2); //"-26" 按位与(&)按位或(|)按位异或(^):只有一个1时才返回1,若对应的两位都是1或都是0,则返回0; 数值大小超过了ECMAScript数值的范围,则返回Infinity或-Infinity1234Infinity * 0 = NaN;Infinity * 非0数值结果是Infinity或-Infinity;Infinity * Infinity = InfinityInfinity / Infinity = NaN 如果是零被零除,结果是NaN非零有限数被零除,结果是Infinity或-Infinity123Infinity +(-Infinity)= NaNInfinity - Infinity = NaN-Infinity - (-Infinity) = NaN +0减-0等于-0-0减-0等于+0 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本;引用类型的值是对象,保存在堆内存中,堆地址指针保存在栈内存中;包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针。基本类型的参数只能按值传递,而在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,局部变量变化会反映在函数的外部 typeof检测基本数据类型:typeof null 会输出objecttypeof检测正则表达式时会输出function,但在IE和firefox中会输出object instanceof检测引用类型(对象) 执行环境和作用域链执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象。执行环境有全局执行环境和函数执行环境之分。全局变量和函数都是作为window对象的属性和方法创建的。 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终是当前执行的代码所在环境的对象。全局执行环境的变量对象始终都是作用域链中的最后一个对象。内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。(通俗地讲,就是可以从里到外调用变量和函数,但外一层不能调用里面一层的) 延长作用域链的方法:try-catch语句的catch块;with语句(不推荐使用)。 Javascript没有块级作用域所谓块级作用域,就是指循环语句块、条件语句块的执行环境,在JavaScript中是不存在的,JavaScript中只有全局作用域以及函数作用域。(ES6中已支持块级作用域)123for(var i=;i<10;i++){ //dosomething} 在javascript里for语句创建的变量即使for循环执行结束后,也依旧会存在于循环外部的执行环境中。 查询标识符(变量、函数名、属性等):搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。 JavaScript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。JavaScript中最常用的垃圾收集方式是标记清除,另外一种不太常见的垃圾收集策略是引用计数。 当数据不再有用,则最好通过将其值设置为null来释放其引用——这个做法叫做解除引用。 创建object实例的方式使用new操作符后跟object构造函数;使用对象字面量(不会调用object构造函数);(对象字面量是定义对象的一种简写形式,也就是一种语法糖,目的在于简化创建包含大量属性的对象的过程)eg:var person={ name :”Peter”, age : 22, 5 : true};这里的数值属性名会自动转换为字符串。 Array数组创建数组的基本方式:使用Array构造函数:var colors = new Array();使用数组字面量表示法:var colors = [];数组的length不是只读,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。12var colors=["red","blue","green"];colors[colors.length]="black"; //在末尾添加一种颜色 使用instanceof操作符和Array.isArray()方法检测数组 数组使用join()方法,则可以使用不同的分隔符来构建这个字符串;123var colors=["red","blue","green"];alert(colors.join(","));alert(colors.join("||")); //red||blue||green 数组的栈方法:push()和pop(); //后进先出push()方法可以接收任意数量的参数,把它们逐个添加到数组末尾,然后返回修改后的数组长度。pop()方法则从数组末尾移除最后一项,减少数组的length值,然后返回移除的项。 数组的队列方法:shift()和push(); //先进先出push()方法在队列的末尾添加项,shift()方法在队列的前端移除项shift()方法能够移除数组中的第一个项并返回该项,同时数组长度减1; unshift()和pop()方法可以实现反向队列,即在数组的前端添加项,在数组的末端移除项。unshift()方法在数组前端添加任意个项并返回数组长度。 数组重排序方法:reverse()和sort(),返回值都是排序后的数组;sort()方法可以接收一个比较函数作为参数。1234567891011function compare(){ if(value1 < value2){ return -1; }else if (value1 > value2){ return 1; }else{ return 0; }}var values=[0,1,5,10,15];values.sort(compare); 对于数值类型或者其valueOf()方法会返回数值类型的对象模型,可以使用更简单的比较函数:123function compare(value1, value2){ return value2-value1;} concat()方法可以基于当前数组中的所有项创建一个新数组。slice()方法可以基于当前数组中的一项或多个项创建一个数组。slice()方法接受一或两个参数,即要返回项的起始和结束位置。(如果slice()方法的参数有一个负数,则用数组长度加上该负数来确定相应的位置,如果结束位置小于起始位置,则返回空数组) 最强大的数组方法—splice()splice()的主要用途是向数组的中部插入项。使用splice的三种方式:删除:可以删除任意数量的项,需指定2个参数:要删除的第一项的位置和要删除的项数。例如,splice(0,2)会删除数组前两项。 插入:可以向指定位置插入任意数量的项,需提供3个参数:起始位置,0(要删除的项数)和要插入的项。如果要插入多个项,可以传入第四、第五,以至任意多个项。例如,splice(2,0,”red”,””green”)会从当前数组位置2开始插入字符串”red”和”green”; 替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,需指定3个参数:起始位置,要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。例如,splice(2,1,”red”,””green”)会删除当前数组位置2的项,然后再从位置2 开始插入字符串”red”和”green”; 数组的位置方法:indexOf()和lastIndexOf()这个两个方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引;两个函数返回的都是要查找的项在数组中的位置,或者在没找到的情况下返回-1。在比较第一个参数与数组中的每一项是,会使用全等操作符,也就是说,要求查找的项必须严格相等。 数组的迭代方法:every(),filter(),forEach(),map(),some()。every()和some()都是对数组中的每一项运行给定的函数,但every()函数只有每一项都是true,才返回true;而some()则是数组中有一项运行结果是true,则返回true。filter()函数是会过滤数组中的一些项,将符合条件的项组成一个新的数组,并返回这个数组。map()函数是将数组中的每一项进行一些操作,然后将操作结果作为新数组中的每一项,并返回这个新数组。 数组的归并方法:reduce()和reduceRight(),这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。reduce()从头开始逐个遍历,reduceRight()则从数组的最后一项开始,向前遍历到第一项。都接收两个参数:在每一项调用的函数和(可选的)作为归并基础的初始值。eg:使用reduce()函数可以求得数组中所有值之和的操作。var values=[1,2,3,4,5];var sum=values.reduce(function(prev,cur,index,array){ return prev + cur ;});reduce()和reduceRight()接收的一个参数—函数接收4个参数,前一个值,当前值,项的索引和数组对象。 Date类型使用valueOf方法返回的是日期的毫秒表示,所以可以使用比较操作符来比较日期值。日期格式化方法:toDateString()——以特定于实现的格式显示星期几、月、日和年;toTimeString——以特定于实现的格式显示时、分、秒和时区;toLocaleDateString——以特定于地区的格式显示星期几、月、日和年;toLocaleTimeString——以特定于实现的格式显示时、分、秒;toUTCString——以特定于实现的格式完整的UTC日期。 RegExp类型Javascript通过RegExp类型来支持正则表达式。1var expression = / pattern / flags; 其中模式(pattern)部分可以是任何简单或复杂的正则表达式,每个正则表达式都可带有一个或多个标志(flags)如下三种标志:g:表示全局模式,即模式将被应用于所有的字符串,而非在发现第一个匹配项是立即停止;i:表示不区分大小写模式;m:表示多行模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。 RegExp实例属性:global: 布尔值,表示是否设置了g标志ignoreCase:布尔值,表示是否设置了i标志lastIndex:整数,表示开始搜索下一个匹配项的字符位置multiline:布尔值,表示是否设置了m标志source:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。 RegExp实例方法:exec(),只接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组,在匹配失败时返回null;返回的数组包含两个额外的属性:index和input。index表示匹配项在字符串中的位置,而input则是应用正则表达式的字符串。1234var test="mom and dad and baby";var pattern = /mom( and dad( and baby)?)?/gi;var matches=pattern.exec(test); 第二个方法是test()方法,接受一个字符串参数,在模式与该参数匹配的情况下返回true;否则返回false.123456var text = "000-00-0000";var pattern = /\d{3}-\d{2}-\d{4}/;if(pattern.test(text)){ alert("This pattern was matched.");} Function类型函数实际上是对象,函数名实际上是一个指向函数对象的指针。函数内部有两个特殊对象:arguments和this.arguments的主要用途是保存函数参数,该对象有一个属性——callee,该属性是一个指针,指向拥有这个arguments对象的函数。使用arguments.callee可以消除函数的执行与函数名的紧密耦合。1234567function factorial(num){ if(num<=1){ return 1; }else{ return num*arguments.callee(num-1); //arguments.callee()等同于factorial() }} this对象也相当重要,详情看JS-this笔记。ECMAScript 5还规范了另一个函数对象属性——caller。这个属性保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。1234567function outer(){ inner();}function inner(){ alert(inner.caller); //输出outer()函数的源码。}outer(); outer调用了inner(),所以inner.caller就指向了outer()。为了实现更松散的耦合,可以通过arguments.callee.caller来访问相同的信息。1234567function outer(){ inner();}function inner(){ alert(arguments.callee.caller); //输出outer()函数的源码。arguments.callee.caller等同于outer}outer(); 总结:arguments.callee是指向拥有arguments对象的那个函数,而arguments.callee.caller是指向调用该函数的函数 每个函数都包含两个属性:length和prototype。length属性是表示函数希望接收的命名参数的个数。prototype属性是不可枚举的,使用for-in无法发现。每个函数都包含两个非继承而来的方法:apply()和call()。这两个函数可以扩充函数赖以运行的作用域。 ps:在严格模式下,未指定环境对象而调用函数,则this值不会转型为window,this值是undefined ECMAScript 5还定义了bind()方法,这个方法会创建一个函数的实例,其this值会被绑定传给bind()函数的值。1234567window.color="red";var obj={color : "blue"};function sayColor(){ alert(this.color);}var objSayColor=sayColor.bind(obj); //将sayColor()函数绑定到obj对象,this指向obj。objSayColor(); 基本包装类型 Boolean、Number、String实际上,每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。 引用类型和基本包装类型的主要区别是对象的生存周期。对基本包装类型的实例调用typeof会返回“object”,而且所有基本包装类型的对象都会被转换成布尔值true. 布尔表达式中的所有对象都会被转换为true.123var falseObj = new Boolean(false);var result = falseObj && true;alert(result); //true Number 类型toFixed()方法,按照指定的小数位返回数值的字符串表示,传入一个参数,是指定显示几位小数。toExponential()方法,返回以指数表示法(e表示法)表示的数值的字符串形式。toPrecision()方法返回某个数值的最合适的格式。 String类型1.字符方法:charAt():返回以单字符字符串的形式返回给定位置的那个字符charCodeAt():返回给定位置的那个字符的字符编码 ECMAScript 5 还定义了另一种访问个别字符的方法,使用方括号加数字索引来访问字符串中的特定字符eg:var str=”helloworld”;alert(str[1]); //输出’e’ 2.字符串操作方法concat():用于将一或多个字符串拼接起来,返回拼接得到的新字符串。(在日常开发中更多的是使用加号操作符+来拼接字符串) slice(),substring(),substr():他们都接收两个参数,slice和substring接收的是起始位置和结束位置(不包括结束位置),而substr接收的则是起始位置和所要返回的字符串长度。当接收的参数是负数时,slice会将它字符串的长度与对应的负数相加,结果作为参数;substr则仅仅是将第一个参数与字符串长度相加后的结果作为第一个参数;substring则干脆将负参数都直接转换为0。 3.字符串位置方法查找子字符串:indexOf()和lastIndexOf(). 4.trim()方法ECMAScript 5中定义了trim()方法,这个方法会创建一个字符串的副本,删除前置以及后缀的所有空格,然后返回结果。 5.字符串大小写转换方法toLowerCase()、toLocaleLowerCase() //转换为小写toUpperCase()、toLocaleUpperCase()//转换为大写 6.字符串的模式匹配方法match():只接收一个参数,要么是一个正则表达式,要么是一个RegExp对象。search():唯一参数与match()方法的参数相同,返回字符串中第一个匹配项的索引;如果没有匹配项,则返回-1;replace():接收两个参数:第一个参数可以上RegExp对象或字符串,第二个参数是一个字符串或函数;split():可以基于指定的分隔符将一个字符串分割成多个子字符串。 7.localeCompare()方法使用本地规则比较字符串string1与string2,并返回比较结果如果string1小于string2,返回-1如果string1大于string2,返回1如果string1等于string2,返回0 8.fromCharCode()方法接收一或多个字符编码,然后将它们转换成一个字符串。 单体内置对象Global和Math对象。所有在全局作用域中定义的属性和函数,都是Global对象的属性。isNaN(),isFinite(),parseInt(),parseFloat(),实际上都是Global对象的方法(在ES6中,parseInt()和parseFloat()是属于Number的方法)。此外,Global还有一些其他方法:1.URI编码方法encodeURI() , encodeURIComponent(); //对URI进行编码decodeURI() , decodeURIComponent(); //对URI进行解码 2.eval()方法将字符串解析为javascript代码。ps:不推荐使用eval(),会损耗性能,还会造成安全问题,如代码注入。 3.window对象Web浏览器都是将Global对象作为window对象的一部分加以实现。 Math对象min()和max()方法,求最小和最大值;舍入方法:向上舍入Math.ceil(),向下舍入Math.floor(),标准舍入(也就是四舍五入)Math.round(). random(),求随机数值 = Math.floor(Math.random()*可能值的总数+第一个可能的值).例如产生1~10的随机数:1Math.random() * 10 + 1]]></content>
<categories>
<category>笔记</category>
</categories>
<tags>
<tag>红宝书</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Github Page + Hexo + NexT搭建个人博客基础篇]]></title>
<url>%2F2017%2F05%2F07%2Fhexo%2Fhexo%2F</url>
<content type="text"><![CDATA[前言: 这篇文章主要记录我如何采用Github Page + Hexo + NexT主题来搭建自己的个人技术博客。文中详细讲解了每一步的操作,小伙伴们看完一遍之后,相信都能够搭建好属于自己的技术博客,搭不好算我输。 Github首先向小伙伴们介绍全球最大的同性交友网站——Github,如果小伙伴们已注册Github账号,请跳过,如若未注册的,那要抓紧啦! Github,全球最大的代码开源共享平台,有无数的优秀代码库,是程序猿成长学习的好地方。同时,个人的Github也是众多优秀程序猿的另一份简历,在众多企业招聘中,拥有活跃的GitHub账号是一个重要的加分项! 总之,好好经营自己的GitHub,将为你敲开众多名企的大门。 拥有GitHub账号后,新建一个项目,命名为:你的GitHub账号.github.io HexoHexo是一个基于Node.js的快速、简洁美观、高效的静态博客框架,支持Markdown、一键部署,还拥有丰富的插件。安装Hexo之前需要先安装Node.js和Git。Mac用户推荐使用iTerm2代替Git,iTerm2是Mac下一款终端神器,集成了git。 安装完Node.js和Git之后,执行以下命令 npm install hexo-cli -ghexo init blogcd blognpm installhexo server 完成上述步骤后,我们便能够看到一个初始的博客网站啦,当然这还不够酷炫,让我们用NexT主题优化下我们的博客吧。 NexTNexT主题是Hexo框架下的一款非常不错的主题,支持三种外观,多种语言,非常简洁美观,这也符合了NexT的宗旨——“精于心,简于形” 首先进入博客项目的文件夹,然后安装NexT主题: cd bloggit clone https://github.com/iissnan/hexo-theme-next themes/next 安装完成后,我们在配置文件(_config.yml)中对主题进行配置.Tips: 在Hexo框架中,由两种配置文件,都是叫做_config.yml,一种在Hexo项目根目录下,一种在主题包(themes)下的各个主题中。我们一般把根目录下的配置文件叫做站点配置文件,另一种在主题中的叫做主题配置文件。 打开站点配置文件,然后修改文件中theme字段: theme: next 主题设置完成,在更改好主题后,最好先执行 hexo clean 命令清除缓存 接着,在本地验证主题是否生效,执行命令启动本地服务器: hexo server –debug 我们可以更改NexT主题的外观,NexT有三种外观,默认为Muse,其他两种为Mist和Pisces。在主题配置文件中,找到scheme字段,选择你想要的主题,注释掉其他主题。 最后,在本地确认博客站点运行正常后,我们将其部署到GitHub Page上去吧!打开站点配置文件,找到deploy字段,将那部分内容更改为: deploy: type: git repo: https://github.com/Peterlhx/peterlhx.github.io branch: master 注意,将repo字段的值更改你自己的GitHub page地址,形式为https://github.com/你的账号/你的账号.github.io 接着,我们执行以下命令: hexo cleanhexo ghexo deploy 至此,博客基本搭建完成,赶快打开你的GitHub page(https://github.com/你的账号/你的账号.github.io) 看看吧! Tips:接下来将进行一些优化操作,例如博客语言的选择、个人头像设定、阅读全文效果、以及一些第三方服务,如评论系统、阅读次数的增加。我们将不断完善自己的技术博客!敬请期待~]]></content>
<categories>
<category>hexo</category>
</categories>
<tags>
<tag>hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hello World]]></title>
<url>%2F2017%2F05%2F07%2Fhexo%2Fhello-world%2F</url>
<content type="text"><![CDATA[Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new "My New Post" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment]]></content>
<categories>
<category>hexo</category>
</categories>
<tags>
<tag>hexo-command</tag>
</tags>
</entry>
</search>