Code Monkey home page Code Monkey logo

segment's Introduction

segment's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

segment's Issues

Vue源码学习笔记之 Dep 和 Watcher

xingbofeng/xingbofeng.github.io#15

image

在初始化的时候

  1. 首先通过 Object.defineProperty 改写 getter/setterData 注入观察者能力

  2. 在数据被调用的时候,getter 函数触发,调用方(会为调用方创建一个 Watcher)将会被加入到数据的订阅者序列;

  3. 当数据被改写的时候,setter 函数触发,变更将会通知到订阅者(Watcher)序列中,并由 Watcher 触发 re-render。后续的事情就是通过 render function code 生成虚拟 DOM,进行 diff 比对,将不同反应到真实的 DOM 中。

JavaScript 词法作用域与动态作用域

改自mqyqingfeng/Blog#3

作用域

指程序源代码中定义变量的区域,它规定了如何查找变量,即确定当前执行代码对变量的访问权限。JavaScript 采用词法(静态)作用域。

词法(静态)作用域与动态作用域

  • JavaScript 采用词法作用域,函数的作用域在函数定义的时候就决定了
  • 动态作用域指作用域在函数调用时才决定,bash便采用动态作用域。
const value = 1

function foo() {
  console.log(value)
}

function bar() {
  var value = 2
  foo()
}

bar()  // 结果是?

因为JavaScript采用静态作用域,因此执行foo函数时,先从作用域内部查找是否有变量value:如果没有,则根据书写的位置,查找上一层代码,即value等于1,因此会打印1。

若JavaScript采用动态作用域,执行foo函数时,依然是从foo函数内部查找是否有变量value:如果没有,就从调用函数的作用域,即函数bar内部查找value变量,因此会打印2。

urlSafeBase64

根据 七牛云文档,可构造如下自定义函数

const urlSafeBase64 = str => {
  str = Base64.encode(str)
  str = str.replace(/\+/g, '-')
  str = str.replace(/\//g, '_')
  return str
}

深入 Vue2.x 的虚拟 DOM diff 原理

改自 https://cloud.tencent.com/developer/article/1006029

一、前言

Vue 的核心是双向数据绑定与虚拟 DOM (以下称为 vdom),vdom 是树状结构,其节点为 vnode,vnode 与浏览器 DOM 中的 Node 一一对应,通过 vnode 的 elm 属性可以访问到对应的 Node

vdom 是纯粹的 JavaScript 对象,因此操作它十分高效,但是 vdom 的变更最终会变为 DOM 操作,为了实现高效的 DOM 操作,一套高效的虚拟 DOM diff 算法很有必要

image

Vue 的 diff 算法如图所示,即仅在同级的vnode间做diff,递归地进行同级 vnode 的 diff,最终实现整个 DOM 树的更新

二、例子

我们在下文中将使用这个简化的例子来讲述 diff 算法的过程
image

如上图所示,更新前是1到10排列的 Node 列表,更新后是乱序排列的 Node 列表。图中有以下几种类型的变化情况:

  1. 头部相同、尾部相同的节点,如 1、10
  2. 头尾相同的节点,如 2、9(处理完第一条之后)
  3. 新增的节点:11
  4. 删除的节点:8
  5. 其它节点:3、4、5、6、7

三、简单的 diff

简单的 diff 算法可以这样设计:逐个遍历 newVDOM 的节点,找到它在 oldVDOM 中的位置,如果找到了就移动对应的 DOM 元素;如果没有找到说明是新增节点,则新建一个节点插入;遍历完成之后,若 oldVDOM 中还有没有处理过的节点,则说明这些节点在 newVDOM 中被删除了,删除它们即可。

存在一个问题:几乎每一步都需要移动 DOM 的操作,这在 DOM 整体结构变化不大时开销是很大的,实际上 DOM 变化不大的情况在现实中经常发生,很多时候我们仅仅需要变更某个节点的文本而已

四、Vue 的 diff 算法实现

上图例子中,存在 oldStart、oldEnd 与 newStart、newEnd 两对儿指针,分别对应 oldVDOM 与 newVDOM 的起点与终点。起止点之前的节点是待处理的节点,Vue 不断对 VNode 进行处理,同时移动指针到其中任意一对起点与终点相遇。处理过的节点 Vue 会在 oldVDOM 与 newVDOM 中同时将它们标记为已处理。Vue 通过以下措施来提升 diff 性能:

一、优先处理特殊场景

  1. 头部的同类型节点,尾部的同类型节点
    这类节点更新前后位置没有发生变化,所以不用移动它们对应的 DOM

  2. 头尾/尾头的同类型节点
    这类节点位置明确,不需要花心思查找,直接移动 DOM 就好

处理了以上场景后,一方面一些不需要做移动的 DOM 得到了快速处理,另一方面待处理节点变少、缩小了后续操作的处理范围,性能也得到提升

二、原地复用

这是指 Vue 会尽可能复用 DOM,尽可能不发生 DOM 的移动。Vue 在判断更新前后指针是否指向同一个节点,其实不要求它们真实引用同一个 DOM 节点,实际上仅判断指向的是否是同类节点(如2个不同的 DIV,在 DOM 上它们是不一样的,但是它们属于同类节点)。如果是同类节点,那么 Vue 会直接复用 DOM,这样的好处是不需要移动 DOM

在看上面的实例,加入10个节点都是 DIV,那么整个 diff 过程就没有移动 DOM 的操作了

五、按步解剖实例

一、整体视图

image

整个 diff 分两部分:

  1. 第一部分是一个循环,循环内部是一个分支逻辑,每次循环只会进入其中的一个分支,每次循环会处理一个节点,处理之后将节点标记为已处理(oldVDOM 与 newVDOM 都要标记;若节点只出现在其中一个 VDOM 中,则另一个 VDOM 不需要标记)。标记方法有两种:当节点正好处于 VDOM 的指针处,移动指针将它排除到未处理列表之外即可,否则 Vue 会将其节点设置为 undefined

  2. 循环结束后,newVDOM 或 oldVDOM 中还存在未处理的节点,如果是 newVDOM 有未处理的节点,则这些节点是新增节点,做新增处理;若 oldVDOM 中还有未处理节点,则这些是需要删除的节点,相应在 DOM 树种删除即可

整个过程是逐步找到更新前后 VDOM 差异,然后将差异反映在 DOM 树中(即 PATCH)。Vue 的 PATCH 是即时的,并不是打包所有修改、最后一起操作 DOM(React 中则是将更新放入队列后集中处理)。现代浏览器对这样的 DOM 操作做了优化,性能上无差异

二、逐步解析

1. 处理头部的同类型节点

即 oldStart 与 newStart 指向同类节点的情况,如下图中节点1。这种情况下,将节点1的变更更新到 DOM,然后对其进行标记,标记方法是 oldStart 与 newStart 后移1位即可,过程中不需要移动 DOM,但可能更新 DOM,如属性变更、文本变更等

image

2. 处理尾部的同类型节点

即 oldEnd 与 newEnd 指向同类节点的情况,如下图中节点10。与情况1类型,这种情况下,将节点10的变更更新到 DOM,然后 oldEnd 与 newEnd前移1位并进行标记,同样不需要移动 DOM

image

3. 处理头尾/尾头的同类型节点

即 oldStart 与 newEnd 以及 oldEnd 与 newStart 指向同类节点的情况,如下图中节点2与9。先看节点2,发生了后移,移动到了 oldEnd 指向的节点(节点9)后,移动之后标记该节点,将 oldStart 后移1位,newEnd 前移一位

image

操作结束之后如下图:

image

同样的,节点9也是类似处理。处理后如下图:
image

4. 处理新增的节点

newStart 来到了节点11的位置,在 oldVDOM 中找不到节点11,说明它是新增的。那么久创建一个新的节点,插入 DOM 树,插到 oldStart指向的节点(节点3)前,然后将 newStart 后移1位标记为已处理(注意 oldVDOM 中没有节点11,所以标记过程中它的指针不需要移动),处理后如下图:

image

5. 处理更新的节点

经过第4步后,newStart 来到了节点7的位置,在 oldVDOM 中能找到它而且不在指针位置(查找 oldVDOM 中 oldStart 到 oldEnd 区间内的节点),说明它位置移动了。那么需要在 DOM 树种移动它到 oldStart 指向的节点(节点3)前,与此同时将节点标记为已处理。与前面几种情况不同,newVDOM 中该节点在指针下,可以移动 newStart 进行标记,而在 oldVDOM 中该节点不在指针处,所以采用设置为 undefined 的方式进行标记

image

处理后就变为下图:

image

6. 处理3、4、5、6节点

经过第5步处理后,我们看到 newStart 与 oldStart 又指向了同一个节点(节点3),很简单,按照1中的做法只需要移动指针即可,非常高效。3、4、5、6都是如此,处理完成后如下图:
image

7.处理需要删除的节点

经过前6步处理后(前6步是循环进行的),newStart 跨过了 newEnd,它们相遇啦!而这个时候,oldStart 与 oldEnd 还没有相遇,说明这2个指针之间的节点(包括它们指向的节点,即上图中节点7、8)是此次更新中被删除的节点,那我们在 DOM 树中进行删除。再回到前面,我们对节点7做了标记,标记是为了告诉 Vue 我们已经处理过它了,是需要出现在新 DOM 中的节点,不要删除它,所以在这里只需删除节点8

在应用中也可能会遇到 oldVDOM 的起止点相遇了、但是 newVDOM 的起止点没有相遇的情况,这时需要对 newVDOM 中的未处理节点进行处理,这类节点属于更新中被加入的节点,需要将他们插入到 DOM 树中

image

至此,整个 diff 过程结束了

从一道题浅说 JavaScript 的事件循环

改自 dwqs/blog#61

资料


  • JavaScript 在单线程中执行,所有的任务可看作存放在两个队列中:
    1. 执行队列:存放同步代码的任务
    2. 事件队列:存放异步代码的 MacroTask
  • MicroTask 处于 执行队列 与 事件队列之间,当 JavaScript 执行时,优先执行完所有同步任务,遇到异步任务时就会根据其任务类型存放到对应的队列中。当执行完同步任务后,将执行 MicroTask ,最后执行 MacroTask

  • 浏览器与 Node.js 中 事件循环 有差异,本文基于 Browsing Context
  • Event Loop

有如下题目:

new Promise(resolve => {
  resolve(1)
  Promise.resolve().then(() => console.log(2))
  console.log(4)
}).then(t => {
  console.log(t)
})

console.log(3)

// 输出 4 3 2 1

事件循环

JavaScript 是 单线程 的,即同一时间只能做一件事。为了协调事件、用户交互、脚本、UI 渲染与网络处理等行为以及防止主线程被阻塞,事件循环(Event Loop)应运而生

Event Loop 包含两类:

二者运行是独立的,即每一个 JavaScript 运行的线程环境都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop

任务队列

事件循环是通过任务队列机制协调的:

  1. 在一个 Event Loop 中,可以有一个或多个任务队列(Task Queue)
  2. 一个任务队列便是一系列有序任务(Task)的集合
  3. 每个任务都有自己的任务源(Task Source)
  4. 来自同一个任务源的 Task 存放在同一个任务队列,否则存放在不同的任务队列

在事件循环中,每进行一次循环操作称之为 Tick,每一次 Tick 的任务处理模型是复杂的,关键步骤如下:

  1. 在此次 Tick 中选择最先进入队列的任务,如果有则执行(一次)
  2. 检查是否存在 MicroTasks,如果存在,则不停执行,直至清空 MicroTasks Queue
  3. 更新 Render
  4. 主线程重复上述步骤

阅读规范可知,异步任务分为 Task(MacroTask 宏任务) 与 MicroTask(微任务) 两类,不同的 API 注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行

MacroTask 包括:

  • Script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI 交互事件
  • PostMessage
  • MessageChannel
  • setImmediate(Node.js)

MicroTask

  • Promise.then
  • MutationObserver
  • Process.nextTick(Node.js)

注意:

  • 在 Node.js 中,会优先清空 nextTick Queue,再清空其它 Queue(如 Promise)
  • timers(setTimeout、setInterval)将优先于 setImmediate 执行,因为前者在 timer 阶段执行,后者在 check 阶段执行

setTimeout、Promise 等 API 便是任务源,而进入任务队列的是它们指定的具体任务,来自不同任务源的任务会进入到不同的任务队列:

image

示例

console.log("Script Start")

setTimeout(() => {
  console.log("timeout1")
}, 10)

new Promise(resolve => {
  console.log("promise1")
  resolve()
  setTimeout(() => console.log("timeout2"), 10)
}).then(() => {
  console.log("then1")
})

console.log("Script End")
  1. 事件循环从 MacroTask 队列开始,此时 MacroTask 队列中,只有一个 Script(整体代码)任务;当遇到 Task Source(任务源)时,则会先分发任务到对应的任务队列中。因此,上面例子第一步执行如下图所示:

image

接着遇到了 console.log() 语句,直接输出 Script Start。输出之后,Script 任务继续往下执行,遇到了 setTimeout,它作为一个 MacroTask,会将其任务分发到对应队列中:

image

Script 任务继续执行,遇到了 Promise 实例,Promise 构造函数中第一个参数是在 new 的时候执行的,执行时其中的参数进入执行栈执行;而后续的 .then 则被分发到 MicroTask 的 Promise 队列中去。因此会输出 Promise1,接着执行 resolve 并将 then1 分发到对应队列

构造函数继续执行,遇到了 setTimeout,然后将其对应的任务分发到对应队列:

image

Sciprt 任务继续执行,最后输出 Sciprt End,至此全局任务执行完毕。当执行完一个 MacroTask 后,会检查是否存在 MicroTasks,若存在则执行 MicroTasks 直至清空 MicroTasks

因此,在 Script 执行完毕后,开始查找清空 MicroTask Queue。此时,MicroTasks 中只有 Promise 队列的一个任务 then1,因此直接执行,输出 then1。当所有的 MicroTasks 执行完毕后,表示第一轮循环结束

image

此后开始第二轮循环,第二轮循环依然从 MacroTask 开始。此时,有两个任务:

  1. timeout1
  2. timeout2

取出 timeout1 执行,输出 timeout1。此时 MicroTask Queue 已经没有可执行的任务了,直接开始第三轮循环:

image

第三轮循环依旧从 MacroTask Queue 开始,此时 MacroTask Queue 只有一个 timeout2,取出并直接输出。此时,MacroTask Queue 与 MicroTask Queue 都没有任务了,因此将不会在输出其它东西。综上,输出结果如下:

Script Start
promise1
Script End
then1
timeout1
timeout2

最初的题目

本文最上方题目:

new Promise(resolve => {
  resolve(1)
  Promise.resolve().then(() => console.log(2))
  console.log(4)
}).then(t => {
  console.log(t)
})

console.log(3)

// 输出 4 3 2 1

这段代码流程如下:

  1. Script 任务先运行,首先遇到 Promise 实例,构造函数首先执行,因此输出4。此时 MicroTask 任务有 t2 与 t1
  2. Script 任务继续执行,输出3,至此第一个 MacroTask 执行完成
  3. 执行所有的 MicroTask,先后取出 t2 与 t1,分别输出 2 与 1
  4. 代码执行完毕,输出为 4 3 2 1

为什么 t2 先执行呢?

  • 根据 Promise/A+ 规范:实践中要确保 onFullFilled 与 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行
  • ES6:Promise.resolve 方法允许调用时不带参数,直接返回一个 resolved 状态的 Promise 对象。立即 resolved 的 Promise 对象,是在本轮 Event Loop 结束时,而不是在下一轮 Event Loop 开始时

校招面试

职位

  • 有赞前端

问题

建议

  • 社招,更注重项目经验;校招,更注重基础内容。

职位

  • 小米前端(实习)

问题

console.log(1)

setTimeout(() => {
  console.log(2)
}, 0)

new Promise(resolve => resolve(3)).then(arg => console.log(arg))

setTimeout(() => {
  console.log(4)
}, 300)

console.log(5)
for (var i = 0; i < 10; i++) {
  console.log(i)
}

for (let i = 0; i < 10; i++) {
  console.log(i)
}

职位

  • LeetCode ** 前端

问题

  • 介绍尚相馆 项目;MySQL、Redis 用在了哪些场景?
  • 介绍React与状态(我讲了讲Vue与Vuex)
  • 介绍Jenkins
  • 是否使用webpack自己构建过项目(直接用了脚手架)
  • 设计一个Koa中间件,限制IP在特定时间内不能超过阈值(我使用到了Redis)
  • 如何判断一个数组是单调的?(下方代码没有考虑到数值相等情况)
    image
  • 生成验证码(下方代码判断重复效率不高,可以借助对象键不能重复/Set方法)
    image

职位

  • 香港中文大学深圳研究院 计算机视觉媒体实验室

问题

image
image

我的解答

image
image

职位

  • 薄荷🌿(前端)

问题

  • 哪一个项目最满意?
  • 该项目中Redis的使用场景?
  • 这几年学习感受最深的是?
  • Redis用到了哪些数据类型?
  • Redis与Memcached差异?
  • Koa如何处理高并发?机制?
  • 除了JS还学习哪些语言?JS与PHP差异?

职位

  • Atlas(前端)

问题

  • importrequire差异;怎么导出模块
  • 箭头函数与普通函数差异
  • 实现一个函数,接收参数1是一个函数f,参数2是一个obj,然后返回值是一个函数h,当调用h的时候,其实是在obj的作用域上调用f参数
const fn = (f, obj) => f.bind(obj)

职位

  • 阿里前端(实习)

一面问题

  • Vue.js 双向数据绑定等内部实现
  • SPA路由原理
  • 前后端检验规则如何同时生效
  • Node应用场景,是否适用于处理大批量数据、如何处理?
  • redis与mysql性能分析
  • redis数据丢失怎么办?
  • 是否经历过大型项目协同?
  • Koa用过哪些中间件?
  • Koa 与 express 区别在哪?
  • Docker与其它容器技术区别在哪

职位

  • 新华智联

问题

  • babel原理
  • 使用js如何给一个元素添加class
  • 小程序架构
  • jsonwebtoken vs cookie
  • docker 架构

职位

  • 扇贝单词(实习)

问题

  • Vue.js 双向数据绑定原理
  • ES6中都了解哪些内容
  • this指向分析
  • Koa中间件原理
  • Koa 与其它框架(不限于Node.js)区别?
  • rxjs是干嘛的
  • wepyjs有哪些痛点、是否使用过mpvue
  • 实现元素水平垂直居中
  • Git 冲突处理
  • 是否使用过Webpack

职位

  • 阿里前端(实习)

二面问题

  • HTTP2原理
  • Web前后端安全以及原理、防御
  • WebSocket
  • ES6内容
  • 使用CSS3实现Loading的思路
  • 如何在对象内部实现私有变量
  • 如何实现内部服务隔离(不使用Key来判断)

职位

  • 轻芒(实习)

一面

  • 介绍项目
  • 唠嗑

简单选择排序

每一趟从待排序的数据元素中选择最小(或 最大)的元素作为首元素,直到所有元素排序完为止。
image

// 待排序数组
let arr = [5, 3, 1, 4, 2]

// 缓存数组长度
const length = arr.length

for (let i = 0; i < length - 1; i++) {
    let min = i
    for (let j = i + 1; j < length; j++) {
        if (arr[j] < arr[min]) {
            min = j
        }
    }
    if (min !== i) {
        const temp = arr[i] arr[i] = arr[min] arr[min] = temp
    }
}

console.log(arr)

image

JavaScript 核心概念之作用域和闭包

改自 https://www.w3cplus.com/javascript/scope-closures.html

作用域

4个概念:

  • 函数对象的[[scope]]属性
  • Scope Chain(作用域链)
  • Execution Context(执行上下文)
  • Activation Object(激活对象)

函数对象的[[scope]]属性

JavaScript 中每个函数都表示为一个函数对象(函数实例),因此它拥有对象的属性与方法。除了正常的属性,它还拥有仅供 JavaScript 引擎内部使用、但不能通过代码访问的一系列内部属性。其中一个便是[[scope]]属性。

Scope Chain

内部的[[scope]]属性,包含了该函数在创建时作用域中的所有对象的集合,该集合成为函数的作用域链(scope chain)。当创建一个函数时,其作用域链中保存的对象,就是在创建该函数时,作用域中所有可访问的数据。

例如以下全局函数:

function add(num1, num2) {
  const sum = num1 + num2;
  return sum;
}

当定义add函数后,其作用域链就创建了。函数所在的全局作用域的全局对象被放置到add函数作用域链([[scope]]属性)中。

查看下图,可以看到作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如thiswindowdocument以及全局对象中的add函数(自身)。因此我们可以在全局作用域下的函数中访问windowthis全局变量函数自身

image

Execution Context

执行以下代码:

const total = add(5, 10);

执行该函数创建一个内部对象,称为 Execution Context。执行上下文 定义了一个函数正在执行时的 作用域 环境。

应当区分执行上下文和函数创建时的作用域链对象[[scope]],因为函数定义时的作用域链对象[[scope]]是固定的,而执行上下文会根据不同的运行时环境变化,且函数每执行一次,都会创建单独的执行上下文,因此多次调用函数便会创建多个执行上下文。一旦函数执行完毕,执行上下文便被销毁。

执行上下文对象有自己的作用域链,当创建执行上下文时,其作用域链将使用执行函数[[scope]]属性所包含的对象(即函数定义时作用域链对象)进行初始化。这些值将按照它们在函数中出现的顺序复制到执行上下文作用域链中。

Activation Object

随后,在执行其上下文中创建一个 Activation Object 的新对象。这个激活对象保存了函数中所有形参、实参、局部变量、this 指针 等函数执行时函数内部的数据情况。然后将这个激活对象推送到执行其上下文作用域链顶部

激活对象是一个可变对象,里面的数据随着函数执行时数据的变化而变化。当函数执行结束之后,执行上下文及其作用域链将被销毁,同时销毁激活对象。但如果存在闭包,激活对象就会以另外一种方式存在(闭包产生原因)。

下图显示了执行上下文及其作用域链:
image

从左往右看,第一部分为函数执行时创建的执行上下文,它有自己的作用域链;第二部分为作用域链中的对象,索引1的对象是从[[scope]]作用域链中复制过来的,索引为0的对象是在函数执行时创建的激活对象;第三部分是作用域链中的对象的内容:Activation Object 和 Global Object。

函数在执行时,每遇到一个变量,都会去执行上下文的作用域链顶部、执行函数的激活对象开始向下搜索,如果在第一个作用域链(Activation Object)中找到了,那么就返回这个变量;如果没找到,继续向下寻找、直到找到为止。如果在整个执行上下文中都没有找到这个变量,则该变量被认为是未定义的。因此,函数可以访问全局变量且当局部变量与全局变量同名时,将会使用局部变量。

Closure(闭包)

有如下代码:

function assignEvents() {
  const id = 'xdi9592';
  document.getElementById('save-btn').onclick = function (event) {
    saveDocument(id);
  }
}

assignEvents函数为 DOM 元素分配了一个事件处理程序,这个处理函数就是闭包。为了使该闭包访问变量id,必须创建一个特定的作用域链。

形成过程:
assignEvents函数创建并词法解析后,函数对象assignEvents[[scope]]属性被初始化,作用域链形成。作用域链中包含了全局对象的所有属性与方法(因函数未执行,因此闭包函数未解析)。

assignEvents 开始执行时,创建 执行上下文,在执行上下文的作用域链中创建激活对象,并将激活对象推送到作用域链顶部,在其中保存了函数执行时所有可访问函数内部的数据。激活对象包含变量id

image

当执行闭包时,JavaScript 引擎发现闭包的存在,闭包函数将被解析,为闭包函数创建[[scope]]属性、初始化作用域链。此时,闭包函数对象的作用域链中有两个对象:assignEvents函数执行时的激活对象 与 全局对象(如上图)。

图中闭包函数对象的作用域链与assignEvens函数的执行上下文的作用域链是相同的。因为闭包函数是在assignEvents函数被执行的过程中被定义并且解析的,而函数执行时的作用域是激活对象,闭包函数被解析时作用域正是assignEvents作用域链中的第一个作用域对象:激活对象。同时,由于作用域链的关系,全局对象作用域也被引入到闭包函数的作用域链中。

本段落有问题,需要更正
在词法分析时,闭包函数的[[scope]]属性就已经在作用域链中保存了对assignEvents函数的激活对象的引用,所以当assignEvents函数执行完毕后,闭包函数虽然还未执行,但依然可以访问assignEvents的局部数据,并不是因为闭包函数要访问assignEvents的局部变量id,因此当assignEvents函数执行完毕后,依然保持了对局部变量id的引用。而是不管是否存在变量引用,都会保存对assignEvents的激活对象的作用域对象的引用。 因为在词法分析时,闭包函数没有执行,函数内部根本不知道是否要对assignEvents的局部变量进行访问和操作,所以只能先把assignEvents的激活对象作用域保存起来,当闭包函数执行时,需要访问assignEvents的局部变量,那么再去作用域链中查找。

正因为如此,造成了一个副作用:当有闭包引用时,激活对象不会被销毁,因为它仍然被引用。因此,闭包将会需要更多的内存。

闭包函数执行时创建了自己的执行上下文,其作用域链使用了[[scope]]属性,引用了assignEvents函数的激活对象与全局对象,并且为闭包本身创建了一个新的激活对象。因此,在闭包函数的执行上下文的作用域链中保存了自己的激活对象、外层函数的 执行上下文 的 激活对象 与 全局对象(如下图所示)。

image

结论

JavaScript 引擎内部使用 hook 跟踪函数定义与执行上下文的作用域链,在函数执行时,变量标识符按照从上到下的顺序通过作用域链解析。如果在最后没有找到相同的变量标识符,则抛出undefined的错误。闭包的开销使其作用域链保持了对其执行上下文的激活对象的引用,从而防止激活对象被正常地销毁。因此,闭包函数通常需要更多的内存。

生成卡片、海报

已知有两种解决方案

  • 使用Canvas
  • 使用打水印的方式(推荐)

打水印制作卡片

  1. 准备干净的底板(模板图片)并上传至七牛云
  2. 调用其接口
  3. 如果卡片上元素内容很多,可以使用水印接口3,可以添加、组合水印内容

生成的卡片

image

1. 确定元素内容(本例**6个)

image

2. 制作模板

image

3.组合6个元素的内容、并调用接口

  // 生成卡片
  const cardRes = await axios.post(REDIS_API + '/qiniu/watermark/image', {
    "originURL": "https://staticfile.shaoyaoju.org/20180613/bg3.jpg",
      "paramsData": [
        {
          "type": "image",
          // 小程序码地址
          "image": qrcodeURL,
          "gravity": "SouthWest",
          "dx": "30",
              "dy": "20",
              "ws": "0.2"
        }, {
          "type": "image",
          // 封面图地址
          "image": cover,
          "gravity": "North",
          "dx": "0",
              "dy": "0",
              "ws": "1"
        }, {
          "type": "text",
              // 卡片标题
              "text": name,
              "font": "黑体",
              "fontsize": "1500",
              "fill": "#000000",
              "dissolve": "100",
              "gravity": "West",
              "dx": "30",
              "dy": "200"
        }, {
          "type": "image",
          // 头像地址
          "image": avatar,
          "gravity": "West",
          "dx": "50",
              "dy": "0",
              "ws": "0.1"
        }, {
          "type": "text",
              // 昵称
              "text": nickname,
              "font": "黑体",
              "fontsize": "800",
              "fill": "#333333",
              "dissolve": "100",
              "gravity": "West",
              "dx": "180",
              "dy": "-30"
        }, {
          "type": "text",
              // 时间
              "text": `${time} 正在浏览『微师大』`,
              "font": "黑体",
              "fontsize": "800",
              "fill": "#908c8c",
              "dissolve": "100",
              "gravity": "West",
              "dx": "180",
              "dy": "30"
        }
      ]
  })

其中

await axios.post(REDIS_API + '/qiniu/watermark/image', {})

为自己封装的一个“中间件”,用于处理七牛水印接口,过程如下

const getImageWaterMark = async ctx => {
  let { originURL, paramsData } = ctx.request.body

  let url = originURL + '?watermark/3'

  for (let i of paramsData) {
    let str = ''

    if (i.type === 'image') {

      let { image = '', dissolve = '100', gravity = 'SouthEast', dx = '10', dy = '10', ws = '0' } = i

      image = urlSafeBase64(image)

      dx = String(dx)
      dy = String(dy)
      ws = String(ws)

      str = `/image/${image}/dissolve/${dissolve}/gravity/${gravity}/dx/${dx}/dy/${dy}/ws/${ws}`
    }

    if (i.type === 'text') {
      let { text = '', font = '黑体', fontsize = '240', fill = '#000000', dissolve = '100', gravity = 'SouthEast', dx = '10', dy = '10' } = i

      text = urlSafeBase64(text)
      font = urlSafeBase64(font)
      fill = urlSafeBase64(fill)

      str = `/text/${text}/font/${font}/fontsize/${fontsize}/fill/${fill}/dissolve/${dissolve}/gravity/${gravity}/dx/${dx}/dy/${dy}`
    }

    url += str
  }

  // 由于最后生成的URL会非常长,便调用了阿里云短网址API将URL缩短
  url = await wechat.shortURL(url)

  ctx.body = {
    url
  }
}

urlSafeBase64 可参考https://github.com/juzhiyuan/blog/issues/3

160行代码仿Vue实现极简双向绑定

改自 https://segmentfault.com/a/1190000015375217

  • 未实现数据深度Get、Set

效果图

005y4rcogy1fsl70vrkj3g30aw09iq3y

双向绑定实现思路

  1. 实现数据监听器 Observer,使用 Object.defineProperty() 重写数据的getset,值更新时将调用set(通知订阅者更新数据)
  2. 实现模板编译 Compile,深度遍历 DOM 树,对每个元素的指令模板进行数据替换以及订阅
  3. 实现 Watcher 用于连接 ObserverCompile,能够订阅并收到每个属性变化的通知,执行指令绑定的相应回调函数,从而更新视图
  4. MVVM入口函数整合以上三者

image

实现

https://jsfiddle.net/juzhiyuan/oh460f3a/82/

调用方式

window.onload = function() {
    new customVue({
        el: '#app',
        data: {
            testDataA: '模仿Vue',
            testDataB: '双向数据绑定',
            name: '琚致远'
        }
    })
}

1、创建 customVue 函数

整合数据监听器this._observer()、指令解析器this._compile()以及连接ObserverCompile_watcherTplwatcher池

function customVue(options = {}) {
  // 配置挂载
  this.$options = options
  // 获取DOM
  this.$el = document.querySelector(options.el)
  // 数据挂载
  this._data = options.data
  // watcher池
  this._watcherTpl = {}
  // 传入数据、执行函数、重写数据的get、set
  this._observer(this._data)
  // 传入DOM、执行函数、编译模板、发布订阅
  this._compile(this.$el)
}

2、创建Watcher函数

用于连接ObserverCompile

  1. 在模板编译_compile()阶段发布订阅
  2. 在赋值操作时更新视图
function Watcher(el, vm, val, attr) {
  // 指令对应的DOM元素
  this.el = el
  // customVue 实例
  this.vm = vm
  // 指令对应的值
  this.val = val
  // DOM获取值:value 获取 INPUT 值、innerHTML 获取 DOM 值
  this.attr = attr
  // 更新视图
  this.update()
}

Watcher.prototype.update = function() {
  // 获取 data 的最新值并赋给 DOM、更新视图
  this.el[this.attr] = this.vm._data[this.val]
}

3、数据监听器 _observer()

使用 Object.defineProperty() 遍历 data 、重写所有属性的 Get、Set 操作,当给对象的某个属性赋值时,将触发 Set,因此我们可以在 Set 中监听数据变化,然后触发 Watch 更新视图

customVue.prototype._observer = function (obj) {
    const _this = this
    // 遍历数据
    Object.keys(obj).forEach(key => {
        // 每个数据的订阅池
        _this._watcherTpl[key] = {
            _directives: []
        }
        // 获取属性值
        let value = obj[key]
        // 数据订阅池
        const watcherTpl = _this._watcherTpl[key]
        // 重写数据的 Get、Set
        Object.defineProperty(_this._data, key, {
            // 可删除
            configurable: true,
            // 可枚举(遍历)
            enumerable: true,
            get() {
                console.log(`${key}获取值:${value}`)
                // 获取值得时候直接返回
                return value
            },
            set(newValue) {
                // 改变值得时候,触发 Set
                console.log(`${key}更新值:${newValue}`)
                if (value !== newValue) {
                    value = newValue
                    // 遍历订阅池、遍历所有订阅的地方(v-model、v-bind、{})并触发this._compile()中发布的订阅 Watcher 更新视图
                    watcherTpl._directives.forEach(item => item.update())
                }
            }
        })
    })
}

4、Compile模板编译

  1. 深度遍历 DOM 树,遍历每个节点以及子节点
  2. 将模板中的变量替换为数据,初始化渲染页面视图
  3. 将指令绑定的属性添加到对应订阅池中
  4. 一旦数据发生变化,收到通知、更新视图
customVue.prototype._compile = function(el) {
    const _this = this
    // 获取 #app 的 DOM
    const nodes = el.children
    // 遍历 DOM节点
    for (let i = 0, len = nodes.length; i < len; i++) {
        const node = nodes[i]
        // 递归、深度遍历DOM树
        if (node.children.length) _this._compile(node)
        // 若存在 v-model 属性,且元素为 INPUT 或 TEXTAREA,则监听其 INPUT 事件
        if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
            node.addEventListener('input', (function(key) {
                // 获取 v-model 绑定的值
                const attVal = node.getAttribute('v-model')
                // 将 DOM 替换成属性的数据并发布订阅、在 Set 时更新数据
                _this._watcherTpl[attVal]._directives.push(new Watcher(node, _this, attVal, 'value'))
                return function() {
                    // INPUT 值改变时,将新值赋给数据、触发 Set-->Watch、更新视图
                    _this._data[attVal] = nodes[key].value
                }
            })(i))
        }

        if (node.hasAttribute('v-bind')) {
            var attrVal = node.getAttribute('v-bind')
            _this._watcherTpl[attrVal]._directives.push(new Watcher(node, _this, attrVal, 'innerHTML'))
        }

        const reg = /\{\{\s*([^}]+\S)\s*\}\}/g
        const txt = node.textContent
        if (reg.test(txt)) {
        	node.textContent = txt.replace(reg, (matched, placeholder) =>{
                    // matched匹配的文本节点包括{{}}, placeholder 是{{}}中间的属性名
                    // 所有绑定watch的数据
                    let getName = _this._watcherTpl
                    // 获取对应watch 数据的值
                    getName = getName[placeholder]
                    // 没有事件池 创建事件池
                    if (!getName._directives) getName._directives = []
                    // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
                    getName._directives.push(new Watcher(node, _this, placeholder, 'innerHTML'))

                    return placeholder.split('.').reduce((val, key) => {
                        // 获取数据的值 触发 Get 返回当前值
                        return _this._data[key]
                    },
                    _this.$el)
            })
        }
    }
}

插入排序

每一步将一个待排序的记录,插入到前面已经排好序的有序序列中,直到插完所有元素。
image

// 待排序数组
let arr = [5, 1, 4, 2, 3]

// 缓存数组长度
const length = arr.length

for (let i = 0; i < length; i++) {
    let j = i
    while (j > 0 && arr[j] < arr[j - 1]) {
        const temp = arr[j]
        arr[j] = arr[j - 1]
        arr[j - 1] = temp
        j--
    }
}
console.log(arr)

image

元素水平、垂直居中

固定宽高、绝对定位元素

.element {
  width: 100px;
  height: 100px;
  position: absolute;
  left: 50%;
  top: 50%;

  // 高度一半
  margin-top: -50px;
  // 宽度一半
  margin-left: -50px;
}

缺点

  • 需要知道元素宽高,否则无法精确调整**margin(可通过 JS 获取)

固定宽高、绝对定位元素

.element {
  width: 100px;
  height: 100px;
  position: absolute;
  left: 50%;
  top: 50%;

  // 50% 为自身尺寸一半
  transform: translate(-50%. -50%);
}
  • transform 偏移的百分比值是相对自身大小的

margin: auto

.element {
  width: 100px;
  height: 100px;
  position: absolute;

  left: 50%;
  top: 50%;
  right: 0;
  bottom: 0;
  margin: auto;
}

注意

  • 上下左右均0位置定位
  • margin: auto
  • 可以不设置尺寸(需要是图片这种自身包含尺寸的元素)

Flex

<div class="container">
  <div class="box">一些元素</div>
</div>
.container {
  display: flex;
  justify-content: center;
  align-items: center;
}

行内元素

父级元素设置text-aligin: center;即可

Grid

.container {
  display: grid;
  grid-template-columns: 50px;
  justify-content: center;
}

函数

函数柯里化

将接受多个参数的函数 转换为 接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数

缩小适用范围,创建一个针对性更强的函数

fn(a, b, c) --> fn(a)(b)(c)()

功能

  • 惰性求值
  • 提前传递部分参数

场景:多个连续的箭头函数

ES6 多次柯里化的结果

a => b => c => {xxx}

例如定义函数add

const add = (x, y) => x + y
add(2, 3) // 5

上述方法柯里化后

const add = x => y => x + y

使用方法

add(2)(3)

// 或者

const add2 = add(2)
add2(3) // 5

函数反柯里化

扩大适用范围,创建一个应用范围更广的函数

fn(a)(b)(c)() --> fn(a, b, c)

函数节流

预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期

保证如果电梯第一个人进来后,10秒后准时运送一次,这个时间从第一个人上电梯开始计时,不等待,如果没有人,则不运行

页面滚动加载

当页面滚动到底部时,将触发 Ajax 请求新的数据。当页面滚动频繁时,有可能出现之前请求尚未结束、又开始新请求的情况。此时,应当使用函数节流。

const getListData = () => {
   // 是否在发送 ajax 请求
   let onAjax = false

   return cb => {
      if (!onAjax) {
         onAjax = true
         $.get('/XXX', data => {
            // 将新数据 push 到列表内
            cb(data)
            onAjax = false
         })
      }
   }
}

$(window).scroll(() => {
   if (滚动到加载新数据的范围之内) {
      getListData()
   }
})
function throttle(fn, delay) {
    let last
    let deferTimer
    return function(args) {
        let that = this
        let _args = arguments
        let now = +new Date() 
        if (last && now < (last + delay)) {
            clearTimeout(deferTimer)
            deferTimer = setTimeout(function() {
                last = now
                fn.apply(that, _args)
            }, delay)
        } else {
            last = now
            fn.apply(that, _args)
        }
    }
}

函数防抖

当调用动作T时间后,才会执行该动作,若在这T时间内又调用此动作则将重新计算执行时间

如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。

function debounce(fn, wait) {
    var timer = null
    return function(){
        clearTimeout(timer)
        timer = setTimeout(()=>{
            fn()
        }, wait)
    }
}

function log() {
    console.log(1)
}
window.onscroll = debounce(log, 500)
// 支持 Promise 的 防抖方法
const debounce = (fn, ms = 0) => {
  let timer = null
  let resolves = []

  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      let result = fn(...args)
      resolves.forEach(r => r(result))
      resolves = []
    }, ms)

    return new Promise(r => resolves.push(r))
  }
}

函数分时

解决函数频繁执行产生的性能问题

  • 主动调用
  • 创建一个队列,使用定时器定时取出下一批要处理的项目进行处理、并设置另一个定时器。

面向切面编程 AOP

在保持主逻辑代码不变的前提下,进行额外的功能扩展

场景:测试函数执行效率

const service = () => {
   console.log('函数 service 开始执行')
}

const test = (() => {
   let timeStart

   return {
      before: () => {
         timeStart = (+new Date())
         console.log('计时开始')
      },
      after: () => {
         const end = (+new Date()) - timeStart
         console.log('计时结束,用时:', end)
      }
   }
})()

const aop = (fn, proxy) => {
   proxy.before && proxy.before()
   fn()
   proxy.after && proxy.after()
}

aop(service, test)

// 计时开始
// 函数 service 开始执行
// 计时结束:1

高阶函数

将 函数作为参数 或 返回值是函数 的 函数

JavaScript 深浅拷贝

改自 https://scotch.io/bar-talk/copying-objects-in-javascript

一个对象就是一组属性值的集合,一组属性值关联着一对 Key 与 Value。在 JavaScript 中,几乎所有的对象都来自于 Object,它在原型链的顶端

介绍

分配符(=)并不会真正拷贝一个对象,它仅仅拷贝了对象的 引用

let obj = {
  a: 1,
  b: 2
}

let copy = obj

obj.a = 5
console.log(copy.a) // 输出 5

变量copy引用该对象,因此对象{a: 1, b: 2}标明:有两种方式(obj或者copy)操作我,不管操作哪一个都会改变我

幼稚的拷贝对象方式

循环原对象,拷贝它的每一个属性:

function copy(mainObj) {
  let objCopy = {}
  let key

  for (key in mainObj) {
    objCopy[key] = mainObj[key]
  }

  return objCopy
}

const mainObj = {
  a: 2,
  b:5,
  c: {
    x: 7,
    y: 4
  }
}

console.log(copy(mainObj))

存在的问题

  • 对象objCopy存在一个不同于对象mainObj原型的Object.prototype,这不是我们想要得到的。我们希望拷贝一个完整的原对象
  • 属性描述符没有被拷贝,一个值为falsewritable描述符,在对象objCopy中将为true
  • 上方代码仅仅拷贝对象mainObj可枚举属性
  • 如果原对象中,一个属性对应的值为一个对象,那么它会被拷贝引用而不是

浅拷贝

在没有任何引用的情况下复制源顶级属性时,对象被称为浅层复制,并且存在一个源属性,其值为对象并作为引用复制。如果源值是对象的引用,则它仅将该引用值复制到目标对象。

浅拷贝将复制顶级属性,但嵌套对象在原始(源)和副本(目标)之间共享。

使用 Object.assign() 方法

Object.assign()方法用来拷贝一个或多个源对象自身所拥有的(不包含原型链)且可枚举属性到目标对象

let obj = {
  a: 1,
  b: 2
}

let objCopy = Object.assign({}, obj)
console.log(objCopy) // 输出:{a: 1, b: 2}

objCopy.b = 89
console.log(objCopy) // 输出:{a: 1, b: 89}
console.log(obj) // 输出:{a: 1, b: 2}

上方代码中,我们实现了对obj的拷贝,当改变了对象objCopyb属性的值为89并打印它后,发现本次修改只针对于对象objCopy,没有改变对象obj。这表明我们成功的从原对象生成了一个拷贝对象,并且没有拷贝其引用。

别着急,尽管看上去一切都没问题,但是还记得我们说过的浅拷贝吗?看下面的例子:

let obj = {
  a: 1,
  b: {
    c: 2
  }
}
let newObj = Object.assign({}, obj)
console.log(newObj) // 输出:{a: 1, b: {c: 2}}

obj.a = 10
console.log(obj) // 输出:{a: 10, b: {c: 2}}
console.log(newObj) // 输出:{a: 1, {b: {c: 2}}}

newObj.a = 20
console.log(obj); // { a: 10, b: { c: 2} }
console.log(newObj); // { a: 20, b: { c: 2} }

newObj.b.c = 30
console.log(obj); // { a: 10, b: { c: 30} }
console.log(newObj); // { a: 20, b: { c: 30} }

当执行obj.b.c = 30后,对象obj与对象newObj的对象属性b的属性c值均发生了改变,这是因为Object.assign方法仅实现了浅拷贝newObj.bobj.b都引用了同一个对象,而不是单独对值进行复制。任何对引用值得改变都会影响引用它的其它对象。

注意:在原型链上的属性或者不可枚举的属性都不会被拷贝

let someObj = {
  a: 2
}

let obj = Object.create(someObj, {
  b: {
    value: 2
  },
  c: {
    value: 3,
    enumerable: true
  }
})

let objCopy = Object.assign({}, obj)
console.log(objCopy)
  • 对象someObj对象obj原型链上,它不会被拷贝
  • 属性 b不可枚举属性
  • 属性 c存在可枚举属性描述符,因此它可以被拷贝

深拷贝

深拷贝将复制它遇到的每个对象,副本和原始对象不会共享任何内容,因此它将是原始副本。下面我们解决下使用Object.assign()进行浅拷贝遇到的问题:

使用 JSON.parse(JSON.stringify(object))

这解决了我们之前遇到的问题。 现在newObj.b有副本而不是引用! 这是深拷贝对象的一种方法:

let obj = {
  a: 1,
  b: {
    c: 2
  }
}

let newObj = JSON.parse(JSON.stringify(obj))

obj.b.c = 20
console.log(obj); // { a: 1, b: { c: 20 } }
console.log(newObj); // { a: 1, b: { c: 2 } }

但是上述方法不适用于拷贝用户定义的对象内方法

拷贝对象方法

方法是作为函数对象的属性,我们尝试拷贝对象内方法:

let obj = {
  name: 'jjzhiyuan',
  exec: function() {
    return true
  }
}

let method1 = Object.assign({}, obj)
let method2 = JSON.parse(JSON.stringify(obj))

console.log(method1)

// 输出

/*
* {
*   exec: function exec() {
*     return true
*   },
*   name: "jjzhiyuan"
* }
*/

console.log(method2)

// 输出

/*
* {
*   name: "jjzhiyuan"
* }
*/

结果显示:Object.assign()方法可以用于拷贝对象内方法,JSON.parse(JSON.stringify(obj))无法拷贝

拷贝嵌套对象

指存在引用自身属性的对象,我们尝试实现对其的拷贝:

使用 JSON.parse(JSON.stringify(object))

let obj = { 
  a: 'a',
  b: { 
    c: 'c',
    d: 'd',
  },
}

obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;

let newObj = JSON.parse(JSON.stringify(obj));

console.log(newObj); 

输出如下:
image
因此 JSON.parse(JSON.stringify(obj))不能用于拷贝嵌套对象

使用 Object.assign()

let obj = { 
  a: 'a',
  b: { 
    c: 'c',
    d: 'd',
  },
}

obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;

let newObj2 = Object.assign({}, obj);

console.log(newObj2); 

输出如下:
image
Object.assign()可以实现对嵌套对象的浅拷贝

使用 扩展运算符(...)

const array = [
  "a",
  "c",
  "d", {
    four: 4
  },
];
const newArray = [...array];
console.log(newArray);
// 输出
// ["a", "c", "d", { four: 4 }]

对象初始值设定项中的Spread属性将源对象中的自身可枚举属性复制到目标对象上:

let obj = {
  one: 1,
  two: 2,
}

let newObj = { ...obj }

// { one: 1, two: 2 }

JavaScript 内存空间及 this 关键词详解

改自 https://www.haorooms.com/post/js_neicun_hjcthis

有如下代码:

let a = 20
let b = a
b = 30

console.log(a) // 应输出多少?
let m = {
  a: 10,
  b: 20
}

let n = m
n.a = 15

console.log(m.a) // 应输出多少?
let a = 20

let obj = {
  a: 10,
  c: this.a + 20,
  fn: function () {
    return this.a
  }
}

console.log(obj.c) // 应输出多少?
console.log(obj.fn()) // 应输出多少?

let _obj = obj.fn
console.log(_obj()) // 应输出多少?

栈与堆

JavaScript 并没有严格意义地区分栈内存与堆内存

  1. 基本数据类型:Null、Undefined、Boolean、Number、String、Symbol 存放在栈中,按值访问。
    • 栈内存中包括了变量的标识符和变量的值
    • 基本类型的比较是它们的值的比较
    var a = 1;
    var b = true;
    console.log(a == b);    // true 只进行值的比较
    console.log(a === b);   // false 不仅进行值得比较,还要进行数据类型的比较

image


  1. 引用数据类型:Object、Array、Date、RegExp、Function 存放在堆中,按引用访问。
    • 栈内存中保存了变量标识符和指向堆内存中该对象的指针
    • 堆内存中保存了对象的内容
    • 引用类型的比较是引用的比较
    var obj1 = {};    // 新建一个空对象 obj1
    var obj2 = {};    // 新建一个空对象 obj2
    console.log(obj1 == obj2);    // false
    console.log(obj1 === obj2);   // false

image

因此上方题目可表示为

一、

image
输出为:20

二、

image
为引用复制,n 与 m 指向同一个对象,修改了堆内的对象后,输出为:15。

this

针对函数调用:

  • this 的指向,是在函数被调用时确定的。
  • 若函数独立调用,则 this 指向 undefined ,在非严格模式中,当 this 指向 undefined 时,它会被自动指向全局对象。
  • 函数调用时,若被某个对象所拥有,则 this 指向拥有它的对象。
  • 函数调用时,若被某个对象所拥有:当该对象在全局声明时,无论诸如 obj.fn 在何处被调用,this 指向全局对象;当对象在函数环境中声明时,this 指向 undefined,在非严格模式下,this 转向 全局对象。

console.log(obj.c)

obj 是全局声明,在非严格模式下,this 指向全局对象,因此:

this.a + 20

输出:40


console.log(obj.fn())

fn() 是 obj 对象下的函数,this 指向 obj,输出:10


console.log(_obj())

_obj() 函数独立调用,this 指向全局,输出:20

JavaScript 运行机制详解:Event Loop

改自 http://www.ruanyifeng.com/blog/2014/10/event-loop.html

参照 #41

JavaScript 是单线程

单线程即同一时间只能做一件事,这与它最初的用途有关。早期作为浏览器脚本语言,主要用于与用户交互、操作 DOM。这决定了它只能是单线程,否则(多线程)会带来不可预测的同步问题。

假设 JavaScript 有两个线程A、B,A线程在某个 DOM 节点上添加内容,B线程在这个DOM节点上删除内容,这时浏览器该以哪个线程为准?因此,JavaScript 从诞生时就是单线程。

为了利用多核CPU计算能力,HTML5 提出了 Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,因此本质上还是单线程。

任务队列

单线程意味着所有任务需要排队,前一个任务结束、才会执行下一个任务。如果前一个任务耗时很长,后一个任务不得不等着。

如果排队是因为计算量大,CPU忙不过来,那可以理解;但是CPU通常是闲着的,因为IO设备很慢(例如ajax从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript 的设计者意识到:主线程可以不管IO设备并挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回结果,再去执行被挂起的任务。

因此,任务可以分两种:同步任务与异步任务。

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
  • 异步任务:不进入主线程,而进入任务队列。只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

异步执行运行机制如下(同步执行也是如此,因为它可以被视为没有异步任务的异步执行

  1. 所有同步任务都在主线程上执行,形成一个执行栈
  2. 主线程之外,存在一个任务队列。只要异步任务有了结果,就在任务队列中放置一个事件。
  3. 一旦执行栈中所有同步任务执行完毕,系统就会读取任务队列,查看其中有哪些事件。那些对应的异步任务,结束等待状态,进入执行栈,开始执行。
  4. 主线程重复以上三步

主线程与任务队列示意图:
image

只要主线程空了,就会重复不断地去读取任务队列,这就是JavaScript的运行机制。

事件与回调函数

任务队列是一个事件队列,IO设备完成一项任务,就在任务队列中添加一个事件,表示相关异步任务可以进入执行栈了。主线程读取任务队列,就是读取有哪些事件。

任务队列中的事件,除了IO设备的事件以外,还包括一些用户产生的事件,如 鼠标点击、页面滚动等。只要指定过回调函数,这些事件发生时就会进入任务队列,等待主线程读取。

所谓回调函数,就是会被主线程挂起的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

任务队列是一个先进先出的数据结构,排在前面的事件会优先被主线程读取。主线程读取过程是自动的,只要执行栈清空,任务队列上第一位的事件就自动进入主线程。但是,由于存在后文提到的定时器功能,主线程首先要检查执行时间,某些事件只有到了规定的时间,才能返回主线程。

Event Loop(事件循环)

主线程从任务队列中读取事件,这个过程是循环不断的,因此这种运行机制被称为Event Loop。

image

上图中,主线程运行时,产生堆 与 栈,栈中代码调用各种外部API,它们在任务队列中添加各种事件(click、load、done)。只要栈中代码执行完毕,主线程就会去读取任务队列,依次执行那些事件对应的回调函数。

执行栈中的代码(同步任务),总是在读取任务队列(异步任务)之前运行。请看下面的例子:

const req = new XMLHttpRequest()
req.open('GET', url)
req.onload = function () {}
req.onerror = function () {}
req.send()

上面代码中的req.send方法是ajax向服务器发送数据,它是一个异步任务,意味着只有当前脚本所有代码执行完毕后,系统才会去读取任务队列。因此,它与下面的写法等价:

const req = new XMLHttpRequest()
req.open('GET', url)
req.send()
req.onload = function () {}
req.onerror = function () {}

即指定回调函数的部分(onload与onerror),在send()方法的前后都可以。因为它们属于执行栈的一部分,系统总是执行完它们才会去读取任务队列。

定时器

除了放置异步任务的事件,任务队列还可以放置定时器事件,即指定某些代码在多少时间之后执行。

定时器功能主要由**setTimeout()setInterval()**这两个函数完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。

以下主要讨论setTimeout():

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数:

console.log(1)
setTimeout(function() {
  console.log(2)
}, 1000)
console.log(3)

上方执行结果输出为1、3、2,因为setTimeout()将第二行推迟到1000ms之后才执行。

如果将setTimeout()的第二个参数设置为0,则表示当前代码执行完(执行栈清空)以后,立即执行(0ms间隔)指定的回调函数。(仍旧是最后执行setTimeout)

setTimeout(function() {
  console.log(1)
}, 0)
console.log(2)

上面代码输出总是2、1,因为只有在执行完下方代码后,才会去执行任务队列中的回调函数。因此,setTimeout(fn, 0)的含义是:指定某个任务在主线程最早可得的空闲时间执行。它在任务队列的尾部添加一个事件,因此要等到同步任务和任务队列现有的事件都处理完,才会得到执行。

HTML5 规定了 setTimeout() 的第二个参数的最小值(最短间隔),不得低于4ms,若低于4ms,则自动增加。另外,对于DOM的变动(尤其是设计页面重新渲染的部分),通常不会立即执行,而是每16ms执行一次。这时使用requestAnimation()的效果好于setTimeout()

需要注意的是,setTimeout()只是将事件插入了任务队列,必须等到当前代码(执行栈)执行完,主线程才会执行它指定的回调函数。如果当前代码耗时很长,有可能要等很久,所以没办法保证回调函数一定会在setTimeout()指定的时间执行。

微信小程序码生成并上传至七牛云

const qiniu = require('qiniu')
const axios = require('axios')

/*
* 获取二维码
* https://developers.weixin.qq.com/miniprogram/dev/api/qrcode.html
*/
const genQRcode = async ({
  // B接口 生成无限量的短路径小程序码
  type = 'limit',
  path = '',
  scene = '',
  width = 430,
  auto_color = true,
  is_hyaline = false
}) => {
  // 在此处获取 AccessToken,这是自己封装的获取微信 AccessToken 函数
  const { access_token } = await getAccessToken()
  // 发起请求,生成小程序码
  const qrCodeRes = await axios.post(`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${access_token}`, {
    // 不同类型的获取二维码接口 此处的参数是不一样的
    page: path,
    scene,
    width,
    auto_color,
    is_hyaline
  }, {
    // 一定要添加返回类型
    responseType: 'stream'
  })

  const { hash, key } = await qiniu.uploadReadableStream(qrCodeRes.data)

  return {
    hash, key
  }
}

七牛云上传文件流

/**
* 文件流上传
* https://developer.qiniu.com/kodo/sdk/1289/nodejs#form-upload-stream
*/
const uploadReadableStream = readableStream => {
  return new Promise((resolve, reject) => {
    const config = new qiniu.conf.Config()
    config.zone = qiniu.zone.Zone_z0
    config.useHttpsDomain = true
    config.useCdnDomain = true
    
    // 自己封装的 获取上传凭证的函数,它将返回 Token 与唯一的 Key(UUID)
    const { uptoken, uuid } = getUploadToken()

    const formUploader = new qiniu.form_up.FormUploader(config)
    const putExtra = new qiniu.form_up.PutExtra()
    formUploader.putStream(uptoken, uuid, readableStream, putExtra, (respErr, respBody, respInfo) => {
      if (respErr) {
        reject(respErr)
      } else {
        resolve(respBody)
      }
    })
  })
}

七牛云 获取上传凭证

const uuid = require('uuid/v1')

/*
* 七牛云 获取上传凭证
*/
const getUploadToken = () => {
  // putPolicy规则请查阅 七牛云 Node.js SDK 文档
  const uptoken = putPolicy.uploadToken(mac)
  return {
    uuid: uuid(),
    uptoken
  }
}

需要注意的是,在获取小程序码时

  • 一定要添加返回类型
responseType: 'stream'
  • page必须是已经发布的小程序存在的页面(否则报错)

JSONP

  • JSON with Padding(Padding 指包裹在服务端响应内容中 JSON 外层的函数名称)
  • 它与 Ajax 无任何关系
  • JSONP

优点

  • 兼容性好,在支持创建 script 标签对的旧版本浏览器中依旧可以良好支持。

缺点

  • 仅支持GET方法
  • 动态创建脚本容易产生XSS攻击

原理

由于浏览器同源策略限制,在目标域名端口协议不一致的情况下,将无法访问目标提供的资源。但是,HTML某些标签提供的src属性,不受该策略影响,因此可以通过该种方式获取目标资源。

  1. 前端动态生成 script 标签对,并设置目标 URL 为 src 属性值。其中,目标 URL 通常会包含客户端 callback 参数名。
  2. 服务端接收 callback 参数;服务端生成响应数据,并使用 callback 参数值包裹该响应内容(传递到客户端后作为函数的参数)。
  3. 客户端加载完成目标资源后,响应内容将在 script 中变为正常的函数调用。

实现

需要服务端配合

前端(原生JS)

<script type="text/javascript">
const customFn = function (data) {
   // 在获取到目标资源后,将调用该函数
   console.log(data)
}

// 目标API
const targetURL = 'https://api.shaoyaoju.org/jsonp?callback=customFn';

// 动态创建 script 标签,并设置 src 属性
const script = document.createElement('script');
script.setAttribute('src', targetURL);

// 将 script 标签插入 body,开始调用目标资源
document.getElementsByTagName('body')[0].appendChild(script);
</script>

后端(Koa.js 实现)

const get = async ctx => {
   // 获取 callback 参数
   const { callback } = ctx.query
   // 包裹数据
   const data = callback + `({"data": {"key": "value"}})`
   ctx.body = data
}

效果

image

冒泡排序

对相邻元素进行比较,顺序相反则对换位置。这样子,每一趟都会将最小(或最大)的元素“浮动”到顶端,最终达到完全有序。
image

// 初始化数组
let arr = [5, 1, 3, 2, 4]

// 缓存数组长度
const length = arr.length

for (let i = 0; i < length - 1; i++) {
    for (let j = 0; j < length - 1 - i; j++) {
        if (arr[j] > arr[j + 1]) {
            const temp = arr[j]
            arr[j] = arr[j + 1]
            arr[j + 1] = temp
        }
    }
}

console.log(arr)

image

CSS 常用布局

https://segmentfault.com/a/1190000008789039

一、常用居中方法

居中在布局中常见,假设 DOM 文档结构如下,子元素要在父元素中居中:

<div class="parent">
 <div class="child"></div>
</div>

水平居中

子元素为行内元素或块状元素、宽度是否一定,采取的布局方案不同,下面进行分析:

  • 行内元素:对父元素设置 text-align: center;
  • 定宽块状元素:设置左右margin 值为 auto
  • 不定宽块状元素:设置子元素为display: inline;,然后设置父元素为text-align:center;
  • 通用方案:flex布局

垂直居中

垂直居中对于子元素是单行内联文本、多行内联文本以及块状元素采用的方案是不同的

  • 父元素一定,子元素为单行内联文本:设置父元素的height等于行高line-height
  • 父元素一定,子元素为多行内联文本:设置父元素display:table-cell;display: inline-block;,再设置vertical-align: middle;
  • 块状元素:设置子元素position: fixedposition: absolute;,然后设置margin: auto;
  • 通用方案:flex布局

二、单列布局

image

特征:定宽、水平居中

常见单列布局有两种:

  1. headercontentfooter宽度相同,其一般不会占满浏览器最宽宽度,但当浏览器宽度缩小低于其最大宽度时,宽度会自适应
  2. headerfooter宽度为浏览器宽度,但content不会占满浏览器宽度

对于第一种,通过对headercontentfooter设置统一的widthmax-width、并设置margin: auto;实现居中

<div class="layout">
  <div id="header">头部</div>
  <div id="content">内容</div>
  <div id="footer">尾部</div>
</div>

  .layout{
  /*   width: 960px; *//*设置width当浏览器窗口宽度小于960px时,单列布局不会自适应。*/
    max-width: 960px;
    margin: 0 auto;
  }

对于第二种,header、footer的内容宽度为100%,但headerfooter的内容区以及content统一设置max-width,并通过margin: auto实现居中

<div id="header">
  <div class="layout">头部</div>
</div>
<div id="content" class="layout">内容</div>
<div id="footer">
  <div class="layout">尾部</div>
</div>

  .layout{
  /*   width: 960px; *//*设置width当浏览器窗口宽度小于960px时,单列布局不会自适应。*/
    max-width: 960px;
    margin: 0 auto;
  }

三、二列、三列布局

image

  • 二列布局特征是:侧栏固定宽度,主栏自适应宽度(可以看作去掉一个侧栏的三列布局)
  • 三列布局特征是:两侧两列固定宽度,中间列自适应宽度

一、 float + margin

设置两个侧栏分别向左右浮动,中间列通过外边距给两个侧栏腾出空间、其宽度根据浏览器窗口自适应

<div id="content">
    <div class="sub">sub</div>
    <div class="extra">extra</div>
    <div class="main">main</div>
</div>
  1. 对两边侧栏设置宽度、设置浮动
  2. 对主面板设置左右外边距,margin-left的值为左侧栏宽度;margin-right的值为右侧栏宽度
.sub {
  width: 100px;
  float: left;
}

.extra {
  width: 200px;
  float: right;
}

.main {
  margin-left: 100px;
  margin-right: 200px;
}

注意:

  • 注意 DOM 书写顺序:先写两侧栏,再写主面板,否则侧栏会被挤压到下一行
    image

  • 这种布局简单明了,但是渲染时先渲染了侧边栏,而不是较为重要的主面板

  • 若是左边带有侧栏的二列布局,则去掉右侧栏,不要设置主面板margin-right值;其它操作相同,反之亦然

二、position + margin

通过绝对定位,将两个侧栏固定,同样通过外边距给两个侧栏腾出空间,中间列自适应

<div class="sub">left</div>
<div class="main">main</div>
<div class="extra">right</div>
  1. 对两边侧栏分别设置宽度,设置定位方式为绝对定位
  2. 设置两侧栏的 top: 0;,设置左侧栏left: 0、右侧栏right: 0;
  3. 对主面板设置左右外边距,margin-left 值为左侧栏宽度;margin-right值为右侧栏宽度
.sub, .extra {
  position: absolute;
  top: 0;
  width: 200px;
}

.sub {
  left: 0;
}

.extra {
  right: 0;
}

.main {
  margin: 0 200px;
}
  • 与上一种方法相比,本方法是通过定位来实现侧栏的位置固定
  • 如果中间栏含有最小宽度限制,或是含有宽度的内部元素,则浏览器窗口小到一定程度,主面板与侧栏会发生重叠
  • 如果是左边带有侧栏的二栏布局,则去掉右侧栏,不要设置主面板的margin-right值,其他操作相同。反之亦然

三、圣杯布局(float + -margin + padding + position)

主面板设置宽度为100%,主面板与两个侧栏都设置浮动,常见为左浮动。这时两个侧栏会被主面板挤压下去,通过负边距将浮动的侧栏拉上来,左侧栏的负边距为100%,刚好是窗口的宽度,因此会从主面板下面的左边跑到与主面板对齐的左边、右侧栏此时浮动在主面板下面的左边,设置负边距为负的自身宽度刚好浮动为左右侧栏的宽度,给侧栏腾出空间,此时主面板宽度减小

由于侧栏的负margin 都是相对主面板的,两个侧栏并不会停靠在左右两边,而是跟着缩小的主面板一起向中间靠拢,此时使用相对布局,调整两个侧栏到相应位置

 <div id="bd">         
    <div class="main"></div>        
    <div class="sub"></div>        
    <div class="extra"></div>  
</div> 
  1. 三者都设置浮动
  2. 设置 main 宽度为100%,设置两侧栏宽度
  3. 设置 负边距,sub 设置负边距为100%,extra 设置负边距为负的自身宽度
  4. 设置 main 的 padding 值给左右两个子面板留出空间
  5. 设置两个子面板为相对定位,sub 的 left值为负的 sub 宽度;extra 的 right 值为 负的 extra 宽度
.main {        
    float: left;       
    width: 100%;   
 }  
 .sub {       
    float: left;        
    width: 190px;        
    margin-left: -100%;               
    position: relative;  
    left: -190px;  
}   
.extra {        
    float: left;        
    width: 230px;        
    margin-left: -230px; 
    position: relative; 
    right: -230px;  
 }
#bd {        
    padding: 0 230px 0 190px;   
 }

注意:

  • DOM 元素书写顺序不得改变
  • 当面板的 main 内容部分比两边的子面板宽度小的时候,布局就会乱掉。可以通过设置 main 的 min-width属性或使用双飞翼布局避免问题。
  • 如果是左边带有侧栏的二栏布局,则去掉右侧栏,不要设置主面板的padding-right值,其他操作相同。反之亦然

四、双飞翼布局(float + 负margin + margin)

双飞翼布局和圣杯布局的**有些相似,都利用了浮动和负边距,但双飞翼布局在圣杯布局上做了改进,在main元素上加了一层div, 并设置margin,由于两侧栏的负边距都是相对于main-wrap而言,main的margin值变化便不会影响两个侧栏,因此省掉了对两侧栏设置相对布局的步骤

<div id="main-wrap" class="column">
      <div id="main">#main</div>
</div>
<div class="sub"></div>        
<div class="extra"></div>
  1. 三者都设置向左浮动
  2. 设置main-wrap宽度为100%,设置两个侧栏的宽度
  3. 设置 负边距,sub设置负左边距为100%,extra设置负左边距为负的自身宽度
  4. 设置main的margin值给左右两个子面板留出空间
.main-wrap {        
    float: left;       
    width: 100%;   
 }  
 .sub {       
    float: left;        
    width: 190px;        
    margin-left: -100%;   
}   
.extra {        
    float: left;        
    width: 230px;        
    margin-left: -230px; 
 }
.main {    
    margin: 0 230px 0 190px;
}

注意:

  • 圣杯采用的是padding,而双飞翼采用的margin,解决了圣杯布局main的最小宽度不能小于左侧栏的缺点
  • 双飞翼布局不用设置相对布局,以及对应的left和right值
  • 通过引入相对布局,可以实现三栏布局的各种组合,例如对右侧栏设置position: relative; left: 190px; ,可以实现sub+extra+main的布局
  • 如果是左边带有侧栏的二栏布局,则去掉右侧栏,不要设置main-wrap的margin-right值,其他操作相同。反之亦然

五、flex布局

http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html

Node.js 支持高并发原因

#34

Node.js 提供 Web 服务,Web 服务属于 I/O 密集型应用,这是 Node.js 所擅长的。Node.js 单线程是指:JavaScript 代码只在单线程(主线程)中运行,但是参考下图

image

Node.js 底层中的 libuv(由C++/C编写) 开启了多线程,当同时有多个 I/O 请求时,主线程会创建多个EIO
线程,以提高 I/O 请求处理速度

设计模式

简单工厂模式

类似于现实生活中的工厂,可产生大量相似的商品、实现同样的效果

const CreatePerson = (name, age, sex) => {
  const obj = new Object()
  obj.name = name
  obj.age = age
  obj.sex = sex
  obj.sayName = function () {
    return this.name
  }
  return obj
}

const personA = new CreatePerson('琚致远', '21', '男')
const personB = new CreatePerson('晨阳', '18', '女')

console.log(personA.name) // 琚致远
console.log(personA.age) // 21
console.log(personA.sex) // 男
console.log(personA.sayName()) // 琚致远

console.log(personB.name) // 晨阳
console.log(personB.age) // 18
console.log(personB.sex) // 女
console.log(personB.sayName()) // 晨阳

// 类型均为 object,无法识别对象类型、无法识别它们属于哪个对象的实例
console.log(typeof personA) // object
console.log(typeof personB) // object

console.log(personA instanceof Object) // true

优点

  • 解决多个相似的问题

缺点

  • 无法确定对象类型

复杂工厂模式

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

  • 弱化对象间耦合,防止代码重复。在一个对象中进行类的实例化,可以消除重复性代码。
  • 重复性业务放在父类中,子类继承于父类中的所有成员属性与方法、专注于实现自己的业务逻辑。

场景:开了多个自行车店,每个点有几种型号的自行车出售

// 父类
const BicycleShop = () => {}

BicycleShop.prototype = {
  constructor: BicycleShop,

  /**
  * 卖自行车
  * @param {model} 自行车型号
  */
  sellBicycle: function (model) {
    const bicycle = this.createBicycle(model)
    // 执行 A 业务逻辑
    bicycle.A()

    // 执行 B 业务逻辑
    bicycle.B()

    return bicycle
  },
  createBicycle: function (model) {
    throw new Error('父类是抽象类,不可直接调用,需要子类重写该方法')
  }
}
// 自行车构造函数
function BicycleShop (name) {
  this.name = name
  this.method = () => {
    return this.name
  }
}

BicycleShop.prototype = {
  constructor: BicycleShop,

   /**
   * 卖自行车
   * @param {model} 自行车型号
   */
  sellBicycle: function (model) {
    const bicycle = this.createBicycle(model)
    // 执行 A 业务逻辑
    bicycle.A()

    // 执行 B 业务逻辑
    bicycle.B()

    return bicycle
  },
  createBicycle: function (model) {
    throw new Error('父类是抽象类,不可直接调用,需要子类重写该方法')
  }
}


/**
* 实现原型继承
* @param { Sub } 子类
* @param { Sup } 超类
*/
const extend = (Sub, Sup) => {
  // 定义空函数
  const F = function () {}

  // 设置空函数原型为超类的原型
  F.prototype = Sup.prototype

  // 实例化空函数,并将超类原型引用传递给子类
  Sub.prototype = new F()

  // 重置子类原型的构造器为子类本身
  Sub.prototype.constructor = Sub

  // 在子类中保存超类原型,避免子类与超类耦合
  Sub.sup = Sup.prototype

  // 检查超类原型的构造器是否为原型自身
  if (Sup.prototype.constructor === Object.prototype.constructor) Sup.prototype.constructor = Sup
}

function BicycleChild (name) {
  this.name = name
  // 继承构造函数父类中的属性与方法
  BicycleShop.call(this.name)
}

// 子类继承父类原型方法
extend(BicycleChild, BicycleShop)

// BicycleChild 子类重写父类方法
BicycleChild.prototype.createBicycle = () => {
  const A = () => {
    console.log('执行 A 业务逻辑')
  }

  const B = () => {
    console.log('执行 B 业务逻辑')
  }

  return {
    A,
    B
  }
}

const childClass = new BicycleChild('飞鸽')
console.log(childClass)

结果如下
image

单例模式

产生一个“类”的唯一实例(注意:JavaScript 中并没有真正的类)

有如下场景:点击某个按钮的时候,弹出遮罩层,比如web.qq.com点击登录的时候

方案一

// 生成灰色背景遮罩层
var createMask = function() {
  return document.body.appendChild(document.createElement("div"));
}

$('button').click(function() {
  var mask = createMask();
  mask.show();
})

上方代码的问题是:这个遮罩层是全局唯一的,每次调用createMask都会创建一个新的div,虽然可以在遮罩层隐藏的时候将它移除掉,但这是不合理的。

方案二

在页面一开始就创建好这个div,然后用一个变量引用它。

var mask = document.body.appendChild(document.createElement('div'));

$('button').click(function() {
  mask.show()
})

这样确实在页面只会创建一个遮罩层div,但是问题是:我们或许永远也不会使用它,因此就会浪费一个div,对dom节点的任何操作应当是吝啬的。

如果借助一个变量,来判断是否已经创建过div呢?

var mask;
var createMask = function() {
  if (mask) return mask;
  mask = document.body.appendChild(document.createElement('div'));
  return mask;
}

上方代码完成了一个产生单例对象的函数,不过我们分析下它有什么不妥:

首先,这个函数存在一定副作用,函数体内改变了外界mask的引用,在多人协作时,createMask是一个不安全的函数;此外,mask这个全局变量并不是必须非要不可,修改如下:

方案三

var createMask = (function() {
  var mask;
  return function() {
    return mask || (mask = document.body.appendChild(document.createElement('div')));
  }
})()

闭包 将变量mask包起来,至少对函数createMask来讲,它是封闭的。上方的单例模式还是有缺点的,它只能用于创建遮罩层,加入我又写了一个函数,用来创建唯一的XHR对象,能不能找到一个通用的单例包装器?

JavaScript 中,函数是第一型,意味着函数可以当做参数来传递,以下为最终代码:

方案四

var wrapper = function(fn) {
  var result;
  return function() {
    return result || (result = fn.apply(this, arguments));
  }
}

var createMask = wrapper(function() {
  return document.body.appendChild(document.createElement('div'));
})

用一个变量来保存第一次返回值,如果它已经被赋值过了,那么在以后的调用中优先返回该变量,而真正创建遮罩层的代码是通过回调函数的方式传入到wrapper包装器的,这种方式叫桥接模式

然而wrapper函数还是不完美,它始终需要一个变量result来寄存div的引用,遗憾的是JavaScript 的函数式特性不足以完全消除声明与语句。

babel 原理

  1. 分析代码
  2. 生成抽象语法树 AST
  3. 递归遍历 AST 并进行转译并生产新的 AST
  4. 通过 AST 生成成旧语法或兼容性较高的语法

注意:

  • babel 只是转译新标准引入的语法,比如 ES6 的箭头函数转译成 ES5 的函数
  • 新标准引入的新的原生对象、部分原生对象新增的原型方法、新增的API等(如Proxy、Set等),这些babel是不会转译的,需要用户自行引入polyfill来解决

script 标签的 async 与 defer

image

  • 不添加属性时:当 HTML 解析遇到 <script src="" /> 标签时,会暂停 HTML 的解析并获取、执行 js 资源,然后才继续执行 HTML 解析

  • 添加 async:当 HTML 解析时遇到 <script src="" /> 标签,会同步下载 js 资源。下载完毕后暂停 HTML 解析并执行 JS

  • 添加 defer:当 HTML 解析时遇到 <script src="" /> 标签,会同步下载 js 资源。下载完毕后,直到 HTML 解析完毕才执行 JS

快速排序

  1. 先从数列中选择一个数作为基准数
  2. 分区:将比这个数大的数放在其右边、小于或等于它的数放在其左边
  3. 对左右区间重复第二步,直到各区间只有一个数
    image
    完成一次排序,剩下的部分分成两个区间,每个区间再次进行排序。

先从数列中选择一个数作为基准数
分区:将比这个数大的数放在其右边、小于或等于它的数放在其左边
对左右区间重复第二步,直到各区间只有一个数

const quickSort = arr = >{
    if (arr.length <= 1) return arr

    // 中间值的索引
    const middleIndex = Math.floor(arr.length / 2)
    // 中间值
    const middle = arr.splice(middleIndex, 1)

    // 分为左右两数组
    const left = []
    const right = []

    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < middle) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }

    return quickSort(left).concat(middle, quickSort(right))
}

console.log(quickSort([5, 1, 4, 2, 3]))

image

JavaScript 事件委托

事件委托

为了理解事件委托是什么,你首先需要理解什么是事件监听:事件监听指监听一个事件,当触发时将产生一些行为。

以下是一些常见的 JavaScript 事件:

  1. change:当 HTML 元素发生改变时将触发
  2. click:当用户点击 HTML 元素时触发
  3. mouseover:当用户移动鼠标到某个元素时触发
  4. mouseout:当用户从某个元素上移出鼠标时触发
  5. keydown:当用户敲击键盘上某个按钮(键)时
  6. load:当浏览器加载页面完毕时

addEventListener

为了给 HTML 某个元素添加事件监听,你需要使用 addEventListener 方法

如下是事件监听的例子:

const character = document.getElementById("disney-character");
character.addEventListener("click", showCharactersName);

第一部分:document.getElementById这部分是 event target,在上述例子中指目标元素。但是在 JavaScript 中,event target 形式多种多样:在上述例子中,它是 HTML 元素;在网页被加载到浏览器中,它可以是文档(document)本身,甚至可以是窗口(window);在客户端 JavaScript 中,它可以是包含所有内容的顶层对象。但是在大多数情况下,它就是 HTML 元素

第二部分:事件监听(event listener)

上述示例的事件监听流程

当用户点击带有 id属性、值为disney-character 的 HTML 元素时,将触发事件监听并调用showCharactersName 函数

事件监听被设置在页面加载时,因此当你打开一个网站时,浏览器下载、读取、运行 JavaScript

const character = document.getElementById("disney-character");
character.addEventListener("click", showCharactersName);

上方代码中,当页面加载时,事件监听查找id属性值为disney-character的元素并且设置它的点击事件,当页面加载时若元素存在,那么上方代码将没有问题。但是当页面加载后,又添加了新的元素时,事件监听该怎么实现呢?

事件委托

事件委托解决了上述问题,为了理解事件委托,我们先看下方的Disney Charaters列表

image

上方列表有一些基本的函数方法,可以实现添加人物、勾选人物等功能

这个列表是动态的,当页面加载完毕后,输入值(Mickey、Minnie、Goofy)将被添加进去,但是这将不能够为它们添加事件监听。

查看下方代码:

const checkBoxes = document.querySelectorAll("input");

checkBoxes.forEach(input => input.addEventListener("click", () => alert("Hi"))});

// 当点击某个人物时,应当弹出 Hi 的提示

但是我们看下页面加载时的 HTML:

<ul class="characters">
</ul>

页面加载后的 HTML:

<ul class=”characters”>
 <li>
   <input type=”checkbox” data-index=”0" id=”item0">
   <label for=”item0">Mickey</label>
 </li>
 
 <li>
   <input type=”checkbox” data-index=”1" id=”item1">
   <label for=”item1">Minnie</label>
 </li>
 
 <li>
   <input type=”checkbox” data-index=”2" id=”item2">
   <label for=”item2">Goofy</label>
 </li>
</ul>

// 当页面加载后,输入值被填充进去,但是并没有绑定事件监听

因为上述元素并没有在页面加载时填入,因此点击任意的人物都不会弹出 Hi 的提示,因为它们没有绑定事件监听。

解决方法

事件委托

事件委托的**是:与其监听(列表的)每一个元素,不如寻找一个在页面加载时就存在的元素

在我们的例子中,class属性值为characters的列表将在页面加载时存在,我们可以在这个元素上实现事件监听

<ul class=”characters”> // PARENT - ALWAYS ON THE PAGE
 <li>
   <input type=”checkbox” data-index=”0" id=”char0"> //CHILD 1
   <label for=”char0">Mickey</label>
 </li>
 
 <li>
   <input type=”checkbox” data-index=”1" id=”char1"> //CHILD 2
   <label for=”char1">Minnie</label>
 </li>
 
 <li>
   <input type=”checkbox” data-index=”2" id=”char2"> //CHILD 3
   <label for=”char2">Goofy</label>
 </li>
</ul>

我们可以将事件委托理解为负责任的父母与粗心的孩子,无论父母说什么,孩子都应当听从。最妙的一点是,如果我们添加更多的孩子(子节点),父母总是有一致的响应,因为父母一直存在(页面加载前后)

我们来实现事件监听:

<ul class="characters">
</ul>

<script>
function toggleDone(event) {
  console.log(event.target)
}

const characterList = document.querySelector(".characters")
characterList.addEventListener("click", toogleDone)
</script>

目前我们在一个列表上添加了事件监听,是在characters上而不是每个子元素

console.log(event.target)

当点击某个人物时,目标对象便被返回了:
image

event.target 是对调度事件的对象的引用,即它标志发生事件的 HTML 元素

在我们的例子中,事件便为click,发生事件的对象是<input />label被认为是input对象的一部分,因此出现了两次

console.log(event.currentTarget)

它不同于上方的console.log(event.target)
image

当事件遍历 DOM 时,event.currentTarget标志事件的当前目标,它始终引用事件监听器附加到的元素。 在我们的例子中,事件监听器被附属到列表characters上,因此我们在控制台看到了它。

在 JavaScript 中实现事件委托

因为我们现在知道EVENT.TARGET标识了事件发生的HTML元素,并且我们也知道我们想要侦听的元素(输入元素),所以在 JavaScript 中解决这个问题相对容易

fucntion toggleDone(event) {
  if (!event.target.matches('input')) return
  console.log(event.target)
}

基本上上面的代码说明:如果单击的事件目标与输入元素(input)不匹配,则退出该函数;如果单击的事件目标与输入元素(input)匹配,则调用console.log(event.target)并在该子节点上执行后续JavaScript 代码

这很重要,现在我们可以确信用户单击了正确的子节点,即使在初始页面加载后才加入输入元素

事件冒泡(Event Bubbling)

当点击元素时,发生了什么

每当用户点击它时,它会一直向上移动到 DOM 的顶部,并在被点击的元素的所有父元素上触发点击事件。你并不总是看到这些点击事件,因为并不是一直在监听这些元素,但确实发生了冒泡事件。

由于它的冒泡性质,事件传播基本上意味着无论何时单击 DOM 上的一个输入,你都可以有效地触发整个 Document 上的事件

查看如下例子:

<div class=”one”>
  <div class=”two”>
    <div class=”three”>
    </div>
  </div>
</div>
<script>
  const divs = document.querySelectorAll('div');  
  function logClassName(event) {
    console.log(this.classList.value);
  }
  divs.forEach(div => div.addEventListener('click', logClassName));
</script>

上述例子中:我们有三个DIV元素:div #1div #2div #3。每个div都有自己的事件监听,当我们在浏览器中点击任意一个div时,都会调用logClassName()这个函数

1_lvpjliinlf3tx3s7o3epta

当我点击了 div #3时,将会执行div #3div #2div #1的监听函数,这就是事件冒泡!

image

回到 事件委托 例子

<ul class=”characters”> // PARENT -- This is where the listener is!
 <li>
   <input type=”checkbox” data-index=”0" id=”char0"> //CHILD 1
   <label for=”char0">Mickey</label>
 </li>
 
 <li>
   <input type=”checkbox” data-index=”1" id=”char1"> //CHILD 2
   <label for=”char1">Minnie</label>
 </li>
 
 <li>
   <input type=”checkbox” data-index=”2" id=”char2"> //CHILD 3
   <label for=”char2">Goofy</label>
 </li>
</ul>
<script>
  const characterList = document.querySelector('.characters');
  characterList.addEventListener('click', toggleDone);
</script>

回到我们在事件委托部分中的示例 - 我们只有一个事件监听器,它是在我们的无序列表元素上设置的。当我们单击该父 HTML 元素的子元素(input)时,它触发了我们设置的绑定到无序列表的事件监听函数

由于事件冒泡,你可以将事件监听函数放置在位于HTML子节点上方的单个父HTML元素上,并且只要事件发生在任何子节点上,该事件监听函数就会被执行 - 即使这些子节点在页面加载后被添加

总结——为什么要使用事件委托

如果没有事件委托,则必须将click事件监听器重新绑定到加载到页面的每个新元素,这是复杂且繁琐的。 首先,它会大大增加页面上事件监听器的数量,而更多的事件监听器会增加页面的总内存占用量。 拥有更大的内存占用会降低性能......而糟糕的性能则是一件坏事。 其次,可能存在与绑定和解除绑定事件监听器以及从DOM 中删除元素相关联的内存泄漏问题。

Node.js 琐碎

Node.js 擅长与不擅长的

擅长

  • I/O 密集型应用:因为 Node.js 以异步的方式处理 I/O 请求

不擅长

  • CPU(计算) 密集型:因为 Node.js 运行于单线程
for (let i = 0; i < 10000000000; i++) {
  console.log(i)
}

什么是异步

// 第一步:定义变量
let a = 1;
 
// 第二步:发出指令,然后把回调函数加入异步队列(回调函数并没有执行)
setTimeout(() => {
 console.log(a);
}, 0)

// 第三步:赋值,回调函数没有执行
a = 2;

// 第四步:发出指令,然后把回调函数加入异步队列(回调函数并没有执行)
setTimeout(() => {
 console.log(a);
}, 0)

// 第五步:赋值,回调函数没有执行
a = 3;

// 当所有代码执行完毕,cpu空闲下来了,就会开始执行异步队列里面的回调函数
// 所以最后控制台输出:3 3

网页渲染

案例网页代码如下

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="styles.css" />
  </head>
  
  <body>
    <h1>
      Click the button.
    </h1>
    
    <button type="button">Click me</button>
    
    <script>
      var button = document.querySelector("button");
      button.style.fontWeight = "bold";
      button.addEventListener("click", function () {
        alert("Well done.");
      });
    </script>
  </body>
</html>

浏览器如何渲染网页

  1. 使用 HTML 创建 文档对象模型 DOM
  2. 使用 CSS 创建 CSS对象模型 CSSOM
  3. 基于 DOM 与 CSSOM 执行脚本 Scripts
  4. 合并 DOM 与 CSSOM 形成渲染树 Render Tree
  5. 使用 渲染树 布局(Layout)所有元素
  6. 渲染(Paint)所有元素
    image

HTML

浏览器从上到下读取 HTML 标签,并将其分成节点,形成 DOM
image

CSS

当浏览器发现任何与节点有关的样式时(外部、内部、行内),停止渲染DOM,并利用这些节点创建CSSOM(阻塞渲染)

//外部样式
<link rel="stylesheet" href="styles.css">

// 内部样式
<style>
  h1 {
    font-size: 18px;
  }
</style>

// 行内样式
<button style="background-color: blue;">Click me</button>

CSSOM 节点创建 与 DOM 节点创建类似,随后,二者合并如下
image

JS

浏览器不断构建 DOM 或 CSSOM 节点,直到发现外部行内脚本。由于脚本可能访问或操作之前的 HTML 或 样式,因此浏览器必须停止解析节点、完成构建 CSSOM、执行脚本、然后继续重复(解析器阻塞)。

image

  • 如果 JS 事件改变了页面的某部分,便会引起渲染树重绘,迫使布局渲染过程再次进行。

渲染树 Render Tree

一旦所有节点已被解析、DOM 与 CSSOM 准备合并、浏览器便会构建渲染树。假设节点是单词,那么对象模型是句子、渲染树是整篇文章(整个页面)。
image

布局 Layout

布局阶段需要确定页面上所有元素的大小与位置
image

渲染

该阶段会真正地光栅化屏幕上的像素,把页面呈现给用户。
image

链表 vs 数组

改自 https://www.studytonight.com/data-structures/linked-list-vs-array

链表 vs 数组

Array Linked List
数组是相似类型元素的集合 链表是相同类型元素的有序集合,它们使用指针互相连接
数组支持随机访问,这意味着可以使用索引直接访问元素,如第一个元素是arr [0],第七个元素是arr [6]等。因此,访问数组中的元素很快,时间复杂度为O(1) 链表支持顺序访问,这意味着访问链表中的任何元素/节点,我们必须顺序遍历完整的链表,直到该元素。访问有n个元素的链表,时间复杂度为O(n)。
在数组中,元素以连续的方式存储在存储器中。 分配给新元素的存储器位置地址存储在链表的先前节点中,从而形成两个节点/元素之间的链接。
在数组中,插入和删除操作需要更多时间,因为内存位置是连续且固定的。 在链表中,新元素存储在第一个空闲、可用的存储位置,只有一个开销步骤,即将存储位置的地址存储在链表的前一个节点中。
在编译时,一旦声明了数组,就会分配内存。 它也被称为静态内存分配。 在添加新节点时,在运行时分配内存。 它也被称为动态内存分配。
每一个元素都是独立的,可以通过索引来访问 每一个节点/元素都指向了下一个、上一个或双方节点
数组可以是一维、二维或者多维的 链表可以是单向的、双向的以及循环的
数组大小必须在声明时被指定 链表在运行时随着节点增减大小是可变的。
数组获取栈中分配的内存 链表从堆中获取内存

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.