jcleeon


  • 首页

  • 分类

  • 归档

  • 标签

复杂度分析

发表于 2019-02-18

什么是复杂度?

在程序中,我们所说的复杂度,通常是指一段程序的执行效率和资源消耗,即:时间复杂度和空间复杂度。

时间复杂度:一段代码的执行效率,是对程序时间维度上的时长估算。

空间复杂度:一种数据结构在内存中的资源消耗,是对程序占用的内存大小估算。

什么需要复杂度分析?

通常我们计算一段程序运行的时长和占用内存的大小,是通过统计、监控等方式。

这种程序执行后进行统计的方式,也称之为:事后统计。

事后统计 通常能够得到比较精确得到程序的执行效率分析结果,但是也有一定的局限性。

事后统计的方式,通常复杂,且测试结果对程序运行的环境有较强的依赖性(如硬件的配置高低)。

且无法在程序执行前,预知实现算法的执行效率。

那么,复杂度分析就是对事后统计的一种弥补,弥补了其无法提前对算法评估的能力。

复杂度分析不依赖测试数据,可在程序执行前,就对其算法的执行效率进行初步的分析。

大O复杂度表示法

谈到效率问题,我们首先想到的是,单位时间内发生的频率,更直白的说,就是一个处理一个事物所需时间越短,则效率越高,反之效率就越低。

那么对于程序而言,代码片段的执行效率,就是这段代码的执行所需的时长问题

对于完成相同任务的代码,我们认为,执行完成所需时间越短的代码,效率越高。

那么,怎样不通过事后分析就能得出一段代码的执行效率呢?

先来看一段求和代码:

def nums_sum(nums):
 sum = 0
 for num in nums:
   sum = sum + num
 return sum

我们暂且假设,这段代码在CPU执行时,认为每行代码的执行时间是相同的(事实并非如此),那么每行代码的执行时间是 unitTime。

第2行执行了一次,即 1 unitTime,第3、4行是 for 循环的一部分,那么其执行次数是 nums.length,用 n 代替,那么两行的执行时间就是 2n unitTime,因此总执行时间:T(n) = (2n + 1) * unitTime;

可以看到,代码的执行时间 T(n) 跟 代码的执行次数 (2n + 1) 是成正比的,因此我们可以使用大O进行如下表示:

T(n) = O(f(n))

公式中的 f(n) 表示代码执行次数

O 表示执行时间 T(n) 与f(n)表达式成正比。

那么,可以得出 T(n) = O(2n + 1),再来看一个例子:

def calcFunc():
  sum = 0
  nums_1 = [1, 2, 3, 4]
  nums_2 = [5, 6, 7, 8]
  for n_1 in nums_1:
    for n_2 in nums_2:
      sum = sum + (n_1 * n_2)
  return sum

根据第一个例子分析出代码的总时长应该是:T(n) = (2n2 + n + 3) * unitTime = O(2n2 + n + 3)

这就是大 O 时间复杂度表示法。大 O 时间复杂度并不具体表示代码的真正执行时间,而是表示代码执行时间随代码执行次数增长的变化趋势,所以,也叫作渐进时间复杂度。

表达式:T(n) = O(2n2 + n + 3) ,当 n 足够大时(假设无穷大),那么大 O 表达式中,常量 3、低阶 n 和 2n2 的系数 2,对增长趋势的影响可以忽略不计。因此我们只需要记录一个最大量级 n2,那么用大O表示法就是:T(n) = O( n2),那么示例1就是:T(n) = O(n)

如何进行时间复杂度分析

总的时间复杂度等于量级最大的那段代码的时间复杂度

即只关注代码中的最大量级,比如示例2,代码中存在双重循环,外层循环执行了 n 次,按照乘法法则,内层循环执行了 n2 次,最大量级就是 n2 ,那么它的时间复杂度就是 O( n2 )。同理,示例1中,只有一个循环,因此最大量级是 n,时间复杂度也就是 O(n)。

几种常见的时间复杂度

  1. 常量阶:O(1)
  2. 对数阶:O(logn)
  3. 线性阶:O(n)
  4. 线性对数阶:O(nlogn)
  5. 平方阶:O( n2)、立方阶: O(n2)……k次方阶:O(nk)

常量阶O(1)

看到 O(1),不要误以为是只执行了一行代码,1 只是常量的一种标识,例如,程序中只要不存在循环和递归语句,即使有成千上万行代码,我们也会说它的复杂度是 O(1),即常量阶

O(logn)、O(nlogn)

先看一段代码:

def calc(n):
    i = 1
    while i <= n:
        i = i * 2

从第四行可以看出,i 从 1 开始,每次循环都会乘以 2。当大于 n 时,循环结束。那么它的计算公式就是这样的

20 21 22 23 …2k …2x = n

因此 2x = n 公式转换为 x = log2 n, 因此这段代码的时间复杂度是 O(log2 n)。

再来看一段代码:

def calc(n):
    i = 1
    while i <= n:
        i = i * 3

根据上面的计算方式,我们可以得到时间复杂度是 O(log3 n)。

那么根据对数公式,我们又可以将 O(log3 n) 转换为 log3 2 * log2 n,因为 log3 2 是一个常量,对增长趋势的影响可以忽略,所以我们可以估算为 O(log2 n) 即:O(logn)

我们已经理解了线性阶复杂度: O(logn),再来理解线性对数阶 O(nlogn) 就容易了,根据乘法法则,如果一段代码的时间复杂度是 O(logn), 我们循环执行 n 遍,时间复杂度就变成了O(nlogn)

响应式JavaScript

发表于 2019-01-20 | 分类于 software

译自:The Best Explanation of JavaScript Reactivity

很多响应式 JavaScript 框架(如:Angular,React 和 Vue)都有他们自己的响应式引擎。理解了什么是响应式和它是如何工作的,能够很好的提高开发技巧,以及更有效的使用前端 JavaScript 框架。

响应式系统

当第一次看到 Vue 的响应式系统是如何工作的,你会觉得非常神奇。

以下面简单的 Vue App 为例:

当 price 发生变化时,Vue 知道它应该至少做三件事

  • 更新页面上的 price 值
  • 重新计算表达式 price * quantity ,然后更新到页面上
  • 再次调用 totalPrice 函数,并更新到页面上

等等,你一定在想: “Vue 是怎么知道 price 何时发生变化的呢?以及,它是如何跟踪这一切的?”

当然,JavaScript 通常可不是这样工作的

如果这对我们来说不是很明显,那么我们面临的最大问题是:JavaScript 语言本身通常不是这样工作的

例如:执行下面代码片段:

我们看到,在计算 total = price * quantity 时,即使下面将 price 设置为 20,结果仍然是 20.

在Vue中,每当价格或数量得到更新时,我们都希望total得到更新,我们希望得到:

1
total is 40

很遗憾,JavaScript 并不是响应式的!所以这个结果在现实中是行不通的。为了让 total 可响应,我们需要让 JavaScript 做一些不同的事情。

问题1

我们需要保存计算 total 的方法,以便当 price 或 quantity 发生变化时重新运行并得到新的 total

方案

首先,我们需要一些方法来告诉我们的应用程序,“将我要运行的代码保存下来,我可能会在其他时间调用。” 然后我们将运行代码,如果 price 或 quantity 变量产生变化,再次执行存储下来的代码,进而更新 total。

我们可以通过 record 函数来实现这个过程,这样我们就可以再次运行它。

record 函数的定义比较简单:

我们将 targe 函数存储下来(在我们的例子中是:() => { total = price * quantity }),以便后面的调用,我们可能需要一个 replay (回放)函数,来执行我们存储下来的所有 target。

因此在我们的代码中,我们可以这样:

很简单,对吧?下面是完整的代码,以便你通读一遍并再次加深理解。

问题2

我们可以根据需要继续进行记录目标( targets ),但是如果我们有一个可扩展且健壮的解决方案,岂不是更好?可能是一个负责维护 targets 列表的类,当需要它们重新运行时,这些 target 会收到通知

解决方案:Dep 类(Dependency Class)

解决这个问题的一种方法是,将这种行为封装成一个实现标准观察者模式的类。

因此,如果我们创建类来管理这些依赖( dependencies )(这更近似于 Vue 处理事情的方式),它看起来可能想这样:

注意:我们现在使用 subscribers 取代 storage ,来存储匿名函数。使用 depend 取代 record 函数。使用 notify 取代 replay ,现在来执行以下我们的代码:

依然能够良好运行,并且现在的代码更加可重用。唯一感觉有点奇怪的是 target 的设置 和 运行!

问题3

将来我们计划为每个变量创建一个 dep 实例,那么,如果将监听更新的匿名函数封装起来,岂不是更好?或许一个 watcher 函数能够满足我们的需求。

所以,watcher 可能需要实现如下代码:

然后,我们只进行如下调用:

解决方案:Watcher 函数

在 watcher 函数里,我们可以做一些简单的事情:

上面你已经看到了,watcher 函数将 func 函数赋值给了全局变量 target ,调用 dep.depend() ,添加 target 为 订阅者,调用 target 初始化 total,最后重置 target

现在我们运行下面的代码:

你可能在想,为什么要把 total 设置为全局变量,而不是在需要它的时候把它设置为局部变量呢?嗯,这个是有充分的理由的,在后面将会体现出来。

问题4

现在我们有了 Dep 类,但是我们想让每个变量都有自己的 Dep 实例。在我们继续这个话题前,先来了解一下属性。

我们假设一下,data 的每个属性( price 和 quantity)都有自己的内部 Dep 类

watcher 函数的调用可以变成如下:

当 data.price 被访问时,我们希望 price 属性的 Dep 类 能够将匿名函数添加( dep.depend() )到订阅者列表( subscribers )中,当 data.quantity 被访问时,我们也希望 quantity 属性的 Dep 类 能够将匿名函数添加( dep.depend() )到订阅者列表( subscribers )中。

另外,如果我们还有其他的匿名函数,该函数中只有 data.price 被访问,那么我们只希望对 price 属性的 Dep 类进行添加订阅者。

那么,price 的所有订阅者(target)已经添加完毕了,应该什么时候调用 dep.notify() 呢?显然,应该在 price 更新的时候。因此我们想要得到的效果,就像下面这样:

上面可以看到,我们是不需要显式的调用 dep.notify() 的

为此,我们需要方法来劫持对data 属性( price 和 quantity )的访问。这样一来,当属性被访问时,我们能够将 target 存储进订阅者列表 subscribers, 并且当属性值发生变化时能够执行存储在订阅者列表中的相应 target 函数。

解决方案:Object.defineProperty()

Object.defineProperty() 函数,从 ES5 中被引入。在开始展示它怎样与 Dep 一起使用 前,我们需要先了解下 Object.defineProperty() 的用法,它允许我们定义属性的 setter 和 getter以及属性的一些其他特性(这里只介绍 get 和 set )。如下:

可以看到,它实际上只打印了两条信息,并未获取 get 或者设置 set 任何值,因为,我们在 Object.defineProperty() 中定义的 set 和 get 会覆盖原生的 get 和 get。现在我们进行实现以下原生的功能,对于 get() ,我们期望返回一个值,对于 set() 我们也需要把属性值更新,所以我们需要一个 内部变量 internalValue 作为中间变量,来存储当前的 price 的值。

所以,我们还是有方法能够在属性被访问 get 和被设置 set 值的时候得到通知。使用Object.keys(data),并且对 data 的属性列表进行递归 forEach,可以对 data 的所有属性,做同样的处理,不吗?

将两种方法结合起来

像上面的代码片段(匿名函数 target 中的代码),当执行并获取 price 的值时,我们 price 能够记住这个匿名函数 target ,这样一来,如果 price 发生变化,或者被设置新的值,就会触发这个函数重新调用,因为它知道这行代码依赖了它。所以我们可以这样想:

  • Get => 记住这个匿名函数,我们将会在它发生变化时再次调用
  • Set => 调用保存的匿名函数,更新它的值

或者以我们的 Dep 类为例

  • Price Get => 调用 dep.depend() 来保存当前的 target
  • Price Set => 调用 price 的 dep.notify(),重新调用所以依赖 price 的 target

这和这两种想法,我们的最终代码如下:

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

let data = { price: 10, quantity: 2 }
let target = null

class Dep { // 依赖管理类
constructor() {
// 用于存储依赖的targets,当执行notify()时
// 所有依赖的 target 被调用
this.subscribers = []
}
depend() { // 代替前面使用的 record 函数
if (target && !this.subscribers.includes( target )) {
// 只有当 target 存在且未被订阅过时,进行订阅
this.subscribers.push( target );
}
}
notify() { // 代替前面的 replay 函数
// 执行所有的 target(observer)
this.subscribers.forEach(sub => sub());
}
}

Object.keys(data).forEach(key => {

let internalValue = data[ key ]
// 每个属性内置一个 dep 实例,当属性发生变化时,只触发自带的 dep 实例依赖的所有 target
const dep = new Dep()

Object.defineProperty(data, key, {
get() {
console.log(`Getting price: ${internalValue}`)
// 调用get时存储 target
dep.depend()
return internalValue
},
set(newVal) {
console.log(`Setting price to: ${newVal}`)
internalValue = newVal
// 值发生变化时,发送通知,触发当前dep存储的所有 target 调用
dep.notify()
}
});
})

function watcher(func) {
target = func
target()
target = null
}

watcher(() => {
data.total = data.price * data.quantity
})
console.log(data.total)
data.price = 20
console.log(data.total)
data.quantity = 3
console.log(data.total)

// 运行结果如下:
// Getting price: 10
// Getting price: 2
// 20
// Setting price to: 20
// Getting price: 20
// Getting price: 2
// 40
// Setting price to: 3
// Getting price: 20
// Getting price: 3
// 60

这正是我们希望得到的! price 和 quantity 属性变成了响应式的!无论何时,price 或者 quantity 发生变化时,我们的代码总是能够重新执行,并触发更新。

现在,Vue文档中的插图看上去应该更加明了了

看到包含 setter 和 getter 的紫色圆饼了吗?它看起来很眼熟吧!每一个组件实例都要一个 Watcher实例(蓝色圆饼),它从 getters 中收集依赖(红线),后面一旦 setter 被调用,他就会通知(notify)监视器(watcher)触发更新,进而重新渲染组件。下面是我自己的注解图片:

现在看起来是不是,更明白了?

显然,Vue在幕后是如何做到这一点的更为复杂,但我们现在对其响应式有了基本的认识,对吧!

我们学到了什么?

  • 如何创建一个 Dep 类,来收集依赖项(depend)并重新运行所有依赖项(notify)
  • 如何创建一个 watcher 来管理我们正在运行的,可能需要被添加为一个依赖的代码( target )
  • 如何使用 Object.defineProperty() 来创建 setter 和 getter

递归

发表于 2019-01-13 | 分类于 数据结构与算法

递归并不属于什么语言的特性,只是一种编程技巧。

对于递归的使用,很多初学者都停留在了解,但不深入,并且很难达到熟练使用的程度。

怎样才能更好的理解递归,并熟练使用呢,这是本篇深入学习的目的。

递归是什么?

递归,是一种程序设计思想,从使用形式上看,是函数的自调用,即在函数的内部调用自身。

案例:计算 n 的阶乘

1
2
3
4
5
6
7
8
function recursion(n) {
if (n === 1) {
return 1
}
return n * recursion( n - 1 );
}
recursion(4)
// 4 * 3 * 2 * 1 = 24

从上面我们看到,recursion 函数的内部调用了 recursion(),自调用的形式就是递归的经典使用形式。

注:递归函数内部必须包含终止条件,即递归的出口。否则会造死循环。

递归在底层是如何执行的呢?

先来看一下程序中的函数在内存中什么样的,如下图所示:

如上图所示,程序中的函数,会存储在 代码段 中,代码段只会保存一份相同的代码。

对于递归来说,每个函数其实都是相同的,因此递归函数会被放到栈中,每次递归的函数(不一定只针对递归函数)被存储到栈帧中,依次排列就形成了一个 调用栈。

那么函数为什么要存储在栈中而不是堆中呢?

我们知道,函数的执行顺序其实是及外的,即内层函数全部执行完毕(释放内存),外层函数才能得到释放,这就适用于栈的先进后出的逻辑。

看一下每个栈帧长什么样:

可以看到,忽略其他的内容,输入参数和返回值,代表的就是一个函数调用。

通过上面对函数在内存的简单了解,我们可以把递归函数的调用栈代换成下面这样:

注:因为压栈是从栈底开始,栈底在最上面,那么图中的函数栈,自然也是从上往下的生长。

理解了函数调用的逻辑,我们可以梳理一下递归函数的执行逻辑:

递归函数开时,从 recursion(4) 开始调用(栈帧1),其返回值是 4 * recursion(3); 但是这时我们还不知道函数 recursion(3) 的返回值是多少,所以需要新的栈帧来计算 recursion(3),那么新的栈帧计算的返回值是 3 * recursion(2),这时 recursion(2)的返回值又不知道了,所以又需要新的栈帧来计算 recursion(2),整个过程就是如此循环下去,直到达到终止条件 recursion(1) = 1 ,有了明确的返回值,然后每个栈帧开始依次出栈,最终就能计算出 recursion(4) 的真实返回值了 。

整个入栈过程就是这样的:

1
2
3
4
recursion(4) = 4 * recursion(3)
recursion(3) = 3 * recursion(2)
recursion(2) = 2 * recursion(1)
recursion(1) = 1

出栈过程是这样的:

1
2
3
4
recursion(1) = 1
recursion(2) = 2 * recursion(1) = 2 * 1
recursion(3) = 3 * recursion(2) = 3 * 2 * 1
recursion(4) = 4 * recursion(3) = 4 * 3 * 2 * 1

使用尾递归防止爆栈

每个栈的容量是有限的,且每个栈帧都会占据一些空间,如果递归的太深(维护的栈帧数量过多)的话,则很可能会导致挤爆一个栈。

再来回顾一下我们的递归算法

1
recursion(n) = n * recursion(n - 1)

从上面的栈帧图我们可以看到,每个栈不仅需要记录当前的 n 值,好需要记录下一个函数栈的返回值,然后才能计算出当前栈帧的结果,所以对于上面的算法,使用多个栈是无法避免的。

为了防止爆栈情况的发生,我们使用尾递归的方式,改下一下我们的算法:

1
2
3
4
5
6
function recursion(n, result) {
if (n === 1) {
return result
}
return recursion(n - 1, n * result);
}

可以看到,之前的 n * recursion(n - 1) 替换成了 recursion(n - 1, n * result),而且函数新增了一个参数 result 。

先看一下计算过程:

1
2
3
4
5
6
recursion(4, 1)
= recursion(3, 4 * 1)
= recursion(2, 3 * 4 * 1)
= recursion(1, 2 * 3 * 4 * 1)
= recursion(1, 24)
= 24

通过上面的计算过程,会发现,每个函数的执行,都不在依赖任何变量和返回值,所有的值,只是通过传参的方式计算得出。跟之前的算法过程进行对比,能够看出,当计算到 recursion(1, 24) ,整个计算过程已经技术,不需要再退回到 recursion(2) = 2 * recursion(1) = 2 * 1 这个函数了不是吗?

这就是 尾递归 的妙处,那么它是怎么做到不会发生爆栈的呢?

当我们使用 尾递归 进行计算时,计算机是能够发现这种情况的,并针对 尾递归 的情况,只需要对递归函数分配一个栈帧就可以搞定这些计算,而且不需要关心 n 的值的大小,以及递归的深度,因为我们不在需要多个栈帧进行协作计算了。

那么具体什么样的递归,才属于尾递归呢?

当递归调用时函数体中最后执行的语句,且该语句不参与任何计算(如,return n * recursion(n - 1)就不是尾递归,因为递归函数在调用时,跟变量 n 形成了计算表达式。),并且返回值不属于表达式的一部分时,这个递归才是 尾递归

小程序常见问题整理

发表于 2019-01-02


如上图,小程序框架中数据绑定的原理示意图,js是一个独立的线程,对数据操作后需要经过线程通信,将数据发送给webview 渲染引擎,最终呈现出效果。线程间通信就会暴露两个缺陷:1. 频繁操作会导致通信阻塞 2. 单次通信传递数据量多大,会导致通信长时间被占用,进而导致通信阻塞

优化建议:

1.对一个数组进行增加数据时只对push或者concat的数据进行setData,以减小setData传递的数据量,减少通信时间

let arr = [1, 2, 3, 4, 5]
const self = this
this.setData({
    arr: arr
})
// bad
setTimeout(() => {
    let arr_data = self.data.arr
    arr_data.push(6)
    self.setData({
        arr: arr_data
    })
}, 1000)
//good 通过数组操作下标的方式,操作数据绑定
setTimeout(() => {
    let data_arr = self.data.arr
    let data = {}
    let length = data_arr.length
    data[`arr[${length}`] = 6
    self.setData(data)
}, 1000)

2.避免连续多次调用setData,能够合并的,尽量合并调用

//bad
this.setData({
    data_1: 10
})
this.setData({
    data_2: 100
})
this.setData({
    data_3: 1000
})

//good
this.setData({
    data_1: 10,
    data_2: 100
    data_3: 1000
})

3.避免后台态页面进行 setData,对于用户看不到的页面,尽量不要继续频繁操作setData,比较影响展示页面的交互效果

CSS 实现点9图

发表于 2018-12-21 | 分类于 H5

本文旨在探讨以下几个问题

1. 什么是点9图?

点9图,我们可以将其理解为图片的一种缩放,即将一张图片,按九宫格的方式拆分为9个部分。如下图:

点9图的重点在于 4角,4边,1中心区域.

图片在拆分使用时,①③⑥⑧四个角保持不变,②④⑤⑦四边进行拉伸或重复,⑨中心用来填充剩余部分。

注:点9图是安卓的一种特殊图片形式,文件扩展名为.9.png,常用于聊天气泡背景图等场景。

2. 为什么要使用点9图?

按照UI提供的素材,如果我们单纯的进行等比例缩放,遇到一些边角像素比较复杂(不规则)的情况,进行等比例拉伸时,很可能会造成拉伸变形,如下图所示:

UI提供的聊天消息气泡图

将图片设置背景等比例拉伸后的样子,是不是很丑!

我们真正想要的样子是这样的,这就是通过使用点9图的方式产生的效果

使用点9图的意义,就在于:可以将图片四角不规则的部分截取保持大小不变,只对其可缩放的四边进行拉伸或重复,进而达到等比放大,又不会造成图片变形的效果。

3. CSS 如何实现点9图?

在 Andriod 中是原生支持 .9.png 这种图片形式的,但是 web 没有采用这种形式。

web 可以通过 CSS 来达到与点9图一样的效果。

那就是 border-image-slice,即通过边框截取的形式处理图片。

border-image-slice 属性会将图片分割为9个区域:四个角(①②③④),四个边以(⑤⑥⑦⑧)及中心区域(⑨)。四条切片线,从它们各自的侧面设置给定距离,控制区域的大小。

border-image-slice 拆分区域

①②③④是四个角区域,用来形成最终边界图像的角点。

⑤⑥⑦⑧是四个边区域,通过拉伸/重复来匹配元素的尺寸。

⑨是中心区域,默认情况下会被舍弃,就好像一个图片中间被镂空了一样,如果对 border-image-slice 设置了 fill 属性的话(fill属性可以在任何位置),中心区域将会作为背景图像被缩放(等价于 background-image),以匹配元素的背景尺寸。

border-image-slice 使用形式

boder-image-slice: number|%|fill,使用实例如下:

1
border-image-slice: 7 12 14 5 fill;

7、12、14、5分别表示 top区域高度、 right区域宽度、 bottom区域高度 、left区域宽度

top、right、bottom、right 的值都可以是 number / percentage,number 时表示 px 值,percentage 是 图片的 height 或者 width 的比例。

注: fill 可以出现在任何位置,如:

1
border-image-slice: 7 fill 12 14 5;

border-image-slice 是负责截取图片的,border-image-repeat, border-image-width, border-image-outset 则定义这些被截取的图片将如何使用,下面会详细说明这些属性。

完整使用案例

以下面UI提供的消息框图片为例,如图:

采用点9图的方式,实现的效果如下图:

可以看到,即使图片被缩放很大,也并没有失真

具体CSS代码实现

1
2
3
4
5
6
7
8
.message-cell-content {
/*border: 10px solid #000; // 如果不存在border-image-outset,则设置边框宽度,用来给 border-image 提供一个容器*/
border-image-source: url(data:image/png;base64,...); // 图片路径
border-image-slice: 10 10 10 10 fill; // 每个区域截取宽度为 10px
border-image-width: 10px 10px 10px 10px; // 设置各个区域的图片宽度
border-image-repeat: repeat; // 图片重复或拉伸模式
border-image-outset: 10px 10px 10px 10px; //
}

以上五行代码就可以完整的实现点9图效果。

想要的效果实现了,就开始解释一下border-image-repeat, border-image-width, border-image-outset 三者的用法。

border-image-repeat

1
border-image-repeat:stretch | repeat | round | space;

stretch 拉伸图片以填充边框,强调边框拉伸。

repeat 平铺图片以填充边框,强调边框重复填充(建议使用此方式保持高清效果)。

round 平铺图像。当不能整数次平铺时,根据情况放大或缩小图像。

space 平铺图像 。当不能整数次平铺时,会用空白间隙填充在图像周围(不会放大或缩小图像)

只要记住,该属性主要是负责边框的缩放形式即可。

border-image-width

定义图像边框宽度。假如 border-image-width 大于已指定的 border-width,那么它将向内部(padding/content)扩展。所以在定义时,如果存在border-width,则跟其值设置一致,防止占用 padding/content 空间。

border-image-outset

定义边框图像可超出边框盒的大小。即,如果不想让 border-image 占用 border 空间的话,可以设置 border-image-outset 使其向 border 外部扩展。其属性值跟 border-image-slice 是对应的。

总结,CSS提供的点9图方式,还是很简洁的,只需要几行代码就能实现。重要的是理解 border-image-slice 是如何切割图片和其他几个属性是如何使用切割好的图片的。

参考资料:

[1] MDN:border-image-slice

[2] Web端的“点九图”

字符编码

发表于 2018-12-01 | 分类于 system

ASCII

在计算机刚诞生的时候,采用8个bit作为一个字节( byte ),每个 bit 其实二进制的一位长度,即8bit = 00000000(0) ~ 11111111(255),因此一个字节的最大长度为 255。那么两个字节的最大长度就是 65535, 依次类推。

由于计算机诞生于美国,美国使用的语言又是字母、数字和标点符号组成,因此被编码到计算机中的字符只有127个,我们称这个编码为 ASCII 码。比如大写字母 A 的编码是 65。

后来由于计算机在全球范围得到广泛应用,因为各国语言不同的原因,一个字节就开始不够用了,比如中文就需要至少两个字节才能表示。所以中国也制定了自己的 GB2312 标准。

Unicode

可以想象,像中国一样,每个国家都一套自己的编码标准,那么久很难做到统一。比如中文,在其他国家的标准中就会呈现为乱码,因此,需要一种能够统一的一套编码,那就是 Unicode。

Unicode 把所有语言统一到一套编码中,这样在全球范围内,就不会出现乱码了。Unicode 通常用两个字节表示如整数范围:00000000 00000000 ~11111111 11111111

如果用 Unicode 编码表示 A,那么 A = 00000000 01000001,即在ASCII 中不够两个字节的,需要前面8位补零

UTF-8

Unicode 统一采用两个字节甚至更多的做法也会产生新的问题,虽然做到了统一编码,但是如果我们的文档大多采用英文的话,那么Unicode的两个字节 和 ASCII 的一个字节,在内存中就需要多出一倍的存储空间,所以在内存和传输上就比较浪费资源。

所以,为了尽可能节省内存,UTF-8 编码就应运而生了。跟 Unicode 一样,UTF-8 也是一套统一的编码,但是唯一的不同是,UTF-8 会根据字符实际占用字节的大小,进行编码,比如原本在 ASCII 编码中的英文字母,在 UTF-8 中也只占用一个字节;中文通常占用三个字节。在编码方面,UTF-8做到了统一和节省内存资源。

文本在计算机内存中,仍然统一使用 Unicode 编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。

用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件。

浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:

对称加密和非对称加密

发表于 2018-11-19 | 分类于 system

对称加密

又称私钥加密,加密 与 解密 使用的是同样的密钥(secret key),在密码学中叫做对称加密算法。因为对称加密的效率很高(排除秘钥长度过长情况),所以加密核心中大都用到了对称加密。当然,对称加密的加密和解密效率也要视秘钥长度而定,秘钥长度越短,解密效率越高;秘钥长度越长,解密效率越高。一般在实际应用中,为照顾的加密的安全性和效率,所以会采用适当的秘钥长度。

优点

1.算法公开

2.计算量小

3.加密效率高

缺点

1.秘钥的管理和分发比较困难

2.不够安全

举例小故事

小明和小红,是两个班级的少男和少女,对男女关系还比较害羞,经常让双方的同桌帮忙传递情书。但是另两人苦恼的是的情书,经常会被自己的同桌给偷窥,还开他们玩笑…。于是两个人约到一起(不要在意为什么都坐一起了,还传信的细节。。。),就合计着怎么才能让双方的同桌传递情书的时候,没办法看信的内容呢?聪明的小明灵机一动想到了一个办法 :把信放到一个盒子里啊,然后再加一把数字密码锁,锁的密码数字只有两人彼此知道,把加过密码锁的盒子给同桌,他们当然不知道密码,只能老老实实的送信啦,然后对方在收到盒子之后,用之前商量好的数字密码将锁打开,这样一来就可以安全的看情书了,但是呢,这样也会存在一个问题,就是如果双方的同桌,一不小心偷偷看到了自己开锁时的数字密码,这样的话,情书内容还是会暴露啊!

非对称加密

又称公钥加密,需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(简称公钥)和私有密钥(简称私钥),即常说的公钥加密,私钥加密或私钥加密,公钥加密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。。私钥只能由一方安全保管,不能外泄,而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密,而解密则需要另一个密钥。

非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公用密钥向其它方公开;得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。

优点

安全性高

缺点

解密速度慢:算法强度复杂

举例小故事(结合对称加密)

某天,客户端小李和服务器老王互掐了起来,小李一脸委屈,老王则暴跳如雷。事情是这样的,像往常一样,俩人的通信是通过对称加密进行的,也就是两边协商好密钥,一人保留一份,进行安全通信。但是一次转账的通信,却出现了严重的问题。他们的老板本来是告诉小李,转账给一个尾号为0001的账户转账 4000 元,小李按照指令,就开始给老王发请求,告知老王要向谁转账4000元,老王看到是小李发的请求,确认身份后,毫不犹豫的做了转账处理。

但是没过多久,老板就一脸怒气的找到了老王和小李,说:”我让你们给为0001的账户转账 4000 元,为什么变成了给0002账户转账10000元,谁能告诉我是怎么回事?“。老王和小李听后一脸震惊!然后看向彼此,小李开始质问小王说:”我明明是告诉你给 0001的账户转账 4000 元 ,为什么变成了给0002账户转账10000元了?“,老王听到小李的质问,一时气节!然后怒视小李,气愤的说:”我明明收到的是向0002账户转账10000元,怎么就变成了向 0001的账户转账 4000 元 ,是不是你自己干什么坏事了?“,接下来就是两人互掐的场面!这时,老板看到小李和老王的反应,觉得他们应该不会骗自己也不会骗对方,毕竟大家在一起相处这么长时间了。

于是老板的冷静也体现了出来。先是制止了互掐中的小李和老王,告诉他们先想想是哪里出问题了?冷静下来的小李和老王也开始探讨起来。老板说,既然你俩都没有错,那就应该是发送的请求信息错了?可是怎么会发生这种错误呢,我们不是刚刚进行安全升级,加了密钥了吗?老王也完全冷静了下来,他突然想到了一个关键性的问题,说:”既然两边都没有错,那只能说明信息小李发送的请求信息被篡改了,就是说很有可能我们的密钥算法很可能被哪个王八蛋给泄露了,导致那人伪造了小李发送的请求,也就是将向0001的账户转账 4000 元的请求,改成了向0002账户转账10000元了!”,听到老王的分析,老板点头表示同意。那么接下来就是商量解决办法了。还是经验老到的老王首先想到了办法,说:“不如这样,既然使用对称加密,加密算法容易被人拿到并解密,那么我们就不用对称加密了,改为使用非对称加密。做法是这样的,小李和我各自生成两个密钥,一个是私钥,一个是公钥。我先把自己的公钥给小李,小李每次向我发送请求的时候,带上我的公钥,我拿到公钥之后,再使用自己手里的私钥进行解密,解密成功,就证明请求是小李发过来的无误;机密失败的话,我就认为这个请求是不安全的,不作任何处理,并进行错误提示。这样一来不就安全了吗?”。听到老王的解决方案,老板拍案叫绝,觉得这个方案,虽然解密麻烦了点,但是安全啊!于是就按照老王的方案进行改进。(故事略长,嘿嘿)

Vue动态组件

发表于 2018-11-15 | 分类于 Vue

项目中,经常会遇到不同组件之间进行动态切换,如 Tab 菜单栏。

通常的实现方式,我们会选择二级路由,但是这就需要管理路由,所以使用起来并不是特别方便。

动态组件的方式,给我们带来了更简洁的动态切换组件的方式。

在 Vue 中,通过给 <component> 元素添加 is 属性来实现动态组件切换业务。

is 表示要动态加载的组件名称或组件的选项对象。

1
2
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>

运用

如上图所示需求,具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="app">
<div class="tabbar">
<button class="tab-item" :class="{'tab-item--active' : tabItem === 'tab-item-one'}" @click="tabItem = 'tab-item-one'">tab-item-one</button>
<button class="tab-item" :class="{'tab-item--active' : tabItem === 'tab-item-two'}" @click="tabItem = 'tab-item-two'">tab-item-two</button>
<component class="tab-content" :is="tabItem"></component>
</div>
</div>

Vue.component('tab-item-one', {
template: ` <div>I am tab-item-one</div> `
})
Vue.component('tab-item-two', {
template: `<div>I am tab-item-two</div>`
})
// <input name="check" type="checkbox">
new Vue({
el: '#app',
data: function () {
return {
tabItem: 'tab-item-one'
}
}
})

当点击 button 按钮时,tabItem 的值会被设置为一个组件的名字(tab-item-one or tab-item-two),并赋值给 is 属性,那么 <component> 就会动态的替换为对应名称的组件。这就是动态组件的实现。

但是上面的代码也会存在一个问题,就是动态组件切换时,会把之前的组件销毁,等到再次切换到某一组件时,其实是重新创建了该组件。这样就无法使得组件在切换时,无法保留之前的状态了!

例如,我们把组件 tab-item-one 调整一下:

1
2
3
4
5
6
7
8
Vue.component('tab-item-one', {
template: `
<div>
I am tab-item-one
<input name="check" type="checkbox">
</div>
`
})

页面效果如图所示

2

当切换到组件 tab-item-one ,将 checkbox 勾选框选中为勾选状态,然后选中 tab-item-two 切换到 tab-item-two 组件,再次选中 tab-item-one,我们会发现组件内的checkbox 处于未勾选的状态。即我们tab-item-two 的状态丢失了。

正如我们上面说的,动态组件在切换时,是基于销毁和创建新组件的过程。那么对于需要保留已显示过的组件,如何保留其状态呢?

答案就是,使用 <keep-alive> ,将组件设置为状态持续。

因此我们队代码进行如下修正:

1
2
3
4
5
6
7
8
9
<div id="app">
<div class="tabbar">
<button class="tab-item" :class="{'tab-item--active' : tabItem === 'tab-item-one'}" @click="tabItem = 'tab-item-one'">tab-item-one</button>
<button class="tab-item" :class="{'tab-item--active' : tabItem === 'tab-item-two'}" @click="tabItem = 'tab-item-two'">tab-item-two</button>
<keep-alive>
<component class="tab-content" :is="tabItem"></component>
</keep-alive>
</div>
</div>

这样一来,切换过程中就会将所有的组件状态进行保存了。对于动态组件的使用我们已经了解了,至于如何更优雅的使用,就需要结合实际的场景了。

ES6 Proxy元编程

发表于 2018-10-09 | 分类于 software

译自:Metaprogramming with proxies

Proxy 是什么

Proxy 使你能够拦截和定制对象执行的操作(如获取属性),是一种元编程特性。

下面的例子中,Proxy是一个我们正在拦截操作的代理对象,handler 是处理拦截操作的对象。

在这个例子中,我们只拦截一个简单的操作: get

1
2
3
4
5
6
7
8
9
const target = {}
const handler = {
get(target, propKey, receiver) {
console.log(`get ${propKey}`)
return 123;
}
}
const proxy = new Proxy(target, handler);
proxy.name // get name

编程和元编程

在我们了解 Proxy 是什么,以及它为什么有用之前,应该先来了解下什么是元编程。

编程,可以分为以下两个级别:

  • 基础级别(Base level也叫,程序级别),代码用来处理用户的输入,即上层应用级别的代码
  • 元级别(Meta level),其中的代码用来处理基础基础级别的代码,更接近于底层

基础级别和元级别的代码可能来自于不同语言的,下面这段元程序代码中,元编程语言是 JavaScript,基础编程语言是 Java

1
2
const str = 'Hello' + '!'.repeat(3);
console.log('System.out.println("'+str+'")');

元编程可以采取不同的形式,在先前的例子中,我们使用 console 打印了 Java 的代码。让我们使用 JavaScript 分别用于元编程语言和基础编程语言,典型的例子就是 eval() 函数,它能够动态的对 JavaScript 代码进行求值或编译。当然 eval() 在实际中并不太推荐使用,所以实际的应用场景并不多。

下面示例中,我们使用 eval() 对 5 + 2 进行求值运算

1
console.log(eval(5 + 2)) // 7

其他的一些 JavaScript 看上去不像元编程,如果仔细看的话,它们确实是:

1
2
3
4
5
6
7
8
9
10
// Base level
let obj = {
hello() {
console.log('hello')
}
}
// Meta level
for (const key of Object.keys(obj)) {
console.log(key)
}

这段程序在运行时,遍历自己的结构。这看起来不太像元编程,因为在 JavaScript 中,程序结构和数据结构的概念是比较模糊的。所有的 Object.* 的方法是可以被看做是元编程函数的

元编程的种类

反射元编程,是指程序处理自身的过程。三种反射元编程的区别:

  • 内省(Introspection):运行时检查对象类型
  • 自修改(Self-modification):可以用来改变程序结构
  • 反射(Intercession):可以用来重新定义一些语言层面的操作

看一些例子

内省:Object.keys() 就是执行了自省操作,用来读取一个结构体(看看先前的例子)

自修改:下面的函数 moveProperty 把 source 属性转移到 target 中,通过括号操作符对属性进行访问、赋值操作符和删除操作符进行自修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
function moveProperty(source, propertyName, target) {
target[propertyName] = source[propertyName];
delete source[propertyName];
}
let obj1 = {
prop: 'abc'
}
let obj2 = {}
moveProperty(obj1, 'prop', obj2);
console.log(obj1)
console.log(obj2)

// { prop: 'abc' }

ES5 中不支持调解,但是 Proxy 却填补了这一空白。

Proxy 用法

ES6 中的 Proxy 给 JavaScript 带来了反射(Intercession),它的工作原理如下,可以对对象 obj 执行许多操作,例如:

  • 获取对象 obj 的 prop 属性(obj.prop)
  • 检查对象 obj 是否存在 prop 属性('prop' in obj)

Proxy 是一个特殊的对象,允许我们定制一些对象的操作, Proxy 构造函数需要两个参数:

  • target:如果处理程序 handler 没有任何拦截某个操作,那么该操作将会使用原目标对象(target)的。也就是说 target 为 Proxy 提供了后备操作。某种程度上,Proxy 包装了目标对象 target

  • handler:每个操作,都有相应的处理程序(handler)方法执行该操作,这种方法拦截操作,成为陷阱(trap)(借用词汇:操作系统陷阱)

下面的例子中,handler 拦截了 get 和 has 操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const target = {}
const handler = {
get(target, propKey, receiver) {
console.log(`GET ${propKey}`)
return 110
},
has(target, propKey) {
console.log(`HAS ${propKey}`)
return true
}
}
const proxy = new Proxy(target, handler)
proxy.too
// GET too
'demo' in proxy
// HAS demo

handler 没有没有实现陷阱 set ,因此,设置 proxy.bar 将会指向 target,并对 target.bar 进行设置

1
2
3
proxy.bar = 'abc'
console.log(target.bar)
// abc

函数专用的陷阱(trap)

如果 target 是一个函数,可以使用另外两个拦截操作:

  • applay 进行函数调用, 通过以下方式:

    • proxy()
    • proxy.call(···)
    • proxy.apply(···)
  • construct 构造函数调用,通过以下方式:

    • new proxy(···)

这两个 trap 只对函数有效的原因是很简单的,因为除此之外,你也不能将这些 trap 应用到其他对象上不是吗?

拦截方法调用

如果你想通过 Proxy 拦截一个方法调用,这将会是一个挑战:你可以拦截一个 get 操作,也可以拦截 apaly 操作,但是关于函数调用的拦截是没有的。

因为函数调用通常被看做两个分离的操作:首先 get 检索一个函数,然后使用 apply 调用函数

因此,我们必须拦截 get,并返回一个拦截函数调用的函数。下面的代码演示了如何做到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function traceMethodCall(obj) {
const handler = {
// 通过 get 检索调用的属性
get(target, propKey, receiver) {
const orgMethod = target[ propKey ]
// 访问属性返回函数,以便该属性可以作为函数调用
return function (...args) {
// 将被拦截函数调用的结果返回,this 指向 proxy
const result = orgMethod.apply(this, args)
console.log(propKey + JSON.stringify(args) + ' -> ' + JSON.stringify(result))
return result
}
}
}
return new Proxy(obj, handler)
}

让我们使用下面的 obj 来试验一下 traceMethodCalls()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
multiply(x, y) {
return x * y
},
squared(x) {
return this.multiply(x, x)
}
}
const traceObj = traceMethodCall(obj)
traceObj.multiply(2, 5)
// multiply[2,5] -> 10
traceObj.squared(4)
// multiply[4,4] -> 16
// squared[4] -> 16

可以看到,traceObj 是 obj 的一个函数被追踪版本。

很好的一点是,即使在 traceObj.squared()中调用 this.traceObj, 它同样也会被跟踪。这是因为 this 一直都在引用 proxy

当然,这又不是特别有效的方法方案,例如,可以使用缓存方法。此外,代理本身对性能有影响。

可撤销(revocable)的 Proxy

ES6 中,可以创建 可撤销的 Proxy

1
const {proxy, revoke} = Proxy.revocable(target, handler);

在运算符= 左边,我们使用解构 的方式得到 Proxy.revocable 返回的 proxy 和 revoke

首次调用 revoke() 后,作用于 proxy 的任何操作都将抛出异常,后续的 revoke() 调用,将没有任何效果:

1
2
3
4
5
6
7
const target = {}
const handler = {}
const { proxy, revoke } = Proxy.revocable(target, handler)
proxy.foo = 123
console.log(proxy.foo) // 123
revoke()
console.log(proxy.foo) // TypeError: Revoked

将代理(Proxy)作为原型(prototype)

代理对象 proto 可以作为对象 obj 的原型,从 obj 执行的一些操作可能指向 proto ,例如下面的 get 操作:

1
2
3
4
5
6
7
8
9
const proto = new Proxy({}, {
get(target, propKey, receiver) {
console.log(`GET ${propKey}`)
return target[ propKey ]
}
})
const obj = Object.create(proto)
obj.foo
// GET foo

在 obj 中并不存在 foo 属性,因此会继续向原型对象 proto 中查找,然后出发陷阱(tap) get

转发拦截操作

处理程序 hanlder 未实现陷阱trap的操作,将自动转发的目标对象 target。有时候除了转发操作,我们还想执行一些其他的操作,例如:handler 拦截了所有操作,并进行打印,但是不会去阻止操作专项目标对象 target

1
2
3
4
5
6
7
8
9
10
const handler = {
deleteProperty(target, propKey) {
console.log(`GET ${propKey}`)
return delete target[ propKey ]
},
has(target, propKey) {
console.log(`HAS ${propKey}`)
return propKey in target
}
}

对于每个陷阱,我们首先记录操作的名称,然后通过手动执行将其转发。ES 6有类似模块的对象 Reflect,这有助于为每个陷阱(trap)转发

1
handler.trap(target, arg_1, ···, arg_n)

Reflect 使用形式如下(其中trap指代拦截的方法统称,非Reflect 的方法):

1
Reflect.trap(target, arg_1, ···, arg_n)

如果我们使用 Reflect ,那么先前的例子应该看起来像下面这样:

1
2
3
4
5
6
7
8
9
10
const handler = {
deleteProperty(target, propKey) {
console.log(`GET ${propKey}`)
return Reflect.deleteProperty(target, propKey)
},
has(target, propKey) {
console.log(`HAS ${propKey}`)
return Reflect.has(target, propKey)
}
}

不是所有的对象都可以通过代理透明包装

handler 可以看作是对其目标对象(target)执行的拦截操作——代理包装目标。代理(Proxy)的处理程序对象(handler)类似于代理的观察者或侦听器。它通过指定实现相应的方法(读取属性的get等)来拦截哪些操作。如果缺少操作的处理程序方法,则不会拦截该操作。它只是被转发到目标。

因此,如果处理程序handler是空对象,代理可以透明地包装目标。但是,这并不总是奏效的

包装对象会影响到 this

在深入之前,让我们快速回顾一下包装目标 target 是如何影响 this 的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const target = {
foo() {
return {
thisIsTarget: this === target,
thisIsProxy: this === proxy,
}
}
}
const handler = {}
const proxy = new Proxy(target, handler)
console.log(target.foo())
// { thisIsTarget: true, thisIsProxy: false }
console.log(proxy.foo())
// { thisIsTarget: false, thisIsProxy: true }

可以看到,如果使用 target 调用 target.foo() ,this 是指向 target 对象的;如果我们使用 proxy 调用 proxy.foo() , this 是指向 proxy 对象的。

这样做的目的其实是,如果目标对象(target) this 调用方法,那么 proxy 也会在循环中保持其 this 不变。

无法透明包装的对象

通常,代理的处理程序对象handler 为空对象时,代理将不会改变目标对象 target 的行为。

但是,如果目标对象 target 没有任何拦截(handler 为 {})的Proxy 包裹,那么我们会遇到一个问题:

事情会变的跟我们想的不一样,因为对象的信息关联依赖于目标对象target 是否被包装。

例如,下面 Person 对象的私有属性(WeakMap) _name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name)
}
get name() {
return _name.get(this)
}
}
const jclee = new Person('jclee')
console.log(jclee.name)
// jclee

const proxy = new Proxy(jclee, {})
console.log(proxy.name)
// undefined

可以看到,具有私有属性的 Person 实例,被没有任何拦截的 Proxy 包装之后,会使我们访问不到私有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person2 {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}

const jane = new Person2('Jane');
console.log(jane.name); // Jane

const proxy = new Proxy(jane, {});
console.log(proxy.name); // Jane

不适用私有属性,就不存上面说的问题。

包装内置构造函数的实例

大多数内置构造函数,也是不能被 Proxy 所包装的,他们因此也不能被显示的包装。例如 Date 构造函数:

1
2
3
4
5
6
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();
// TypeError: this is not a Date object.

这种不受 Proxy 影响的机制称为内部槽,这些内部槽是与实例相关联的伪属性。规范处理这些插槽,就好像它们是用方括号括起来的属性一样,例如,下面的方法是内部的,可以在所有对象 o 上调用:

1
O.[[GetPrototypeOf]]()

但是,访问内部插槽并不是通过正常的 get 和 set 操作进行的。如果 getDate() 是通过代理调用的,它就无法在 this 中找到它需要的内部槽位,并会报 TypeError。

对于 Date 方法,语言规范声明:

除非另有明确说明,下面定义的 Number 原型对象的方法不是通用的,传递给它们的这个值必须是一个 Number 值或一个具有 [[NumberData]] 内部槽的对象,该槽已经初始化为一个 Number 值

数组可以被透明包装

与其他内置程序相比,数组可以透明包装。

1
2
3
4
const proxy = new Proxy(new Array(), {})
proxy.push('a')
console.log(proxy.length) // 1
console.log(proxy[0]) // a

之所以数组是可包装的,是因为尽管属性访问是定制的,以使 length 可被访问。数组方法不依赖于内部插槽——它们是通用的

可变通性

作为一种变通方式,我们可以更改处理程序如何转发方法调用,并有选择的设置 this 是指向 target 还是 proxy

1
2
3
4
5
6
7
8
9
10
const handler = {
get(target, propKey, receiver) {
if (propKey === 'getDate') {
return target.getDate.bind(target)
}
return Reflect.get(target, propKey, receiver);
}
}
const proxy = new Proxy(new Date('2020-12-24'), handler);
console.log(proxy.getDate()) // 24

这种方法的缺点是,该方法在此上执行的任何操作都不通过 proxy。

Proxy 使用案例

下面用一些例子来展示 Proxy 的使用场景,并在实际应用中熟悉一下 Proxy 的 API

追踪属性访问(get, set)

假设我们有一个方法 tracePropAccess(obj, propKeys) ,每当设置或者获取 obj 中的属性(且该属性存在于 propKeys 数组中),我们便打印一些信息。在下面的代码中,我们将该函数应用于类 Point 的实例。

1
2
3
4
5
6
7
8
9
10
11
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `Point(${this.x}, ${this.y})`;
}
}
let point = new Point(10, 20)
point = tracePropAccess(point, ['x', 'y']);

设置或者获取被追踪的对象 point 会产生如下效果:

1
2
3
4
5
6
> point.x
GET x
5
> point.x = 21
SET x=21
21

有趣的是,无论何时 point 的属性被访问,追踪都将产生效果。因为 this 是始终指向 追踪对象(Proxy)的,而不是 point

在 ES 5 中,我们可能会像下面这样实现 tracePropAccess(obj, propKeys)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function tracePropAccess(obj, propKeys) {
let propData = Object.create(null)
propKeys.forEach(function (propKey) {
propData[ propKey ] = obj[ propKey ]
Object.defineProperty(obj, propKey, {
get: function() {
console.log('GET '+propKey);
return propData[propKey];
},
set: function(value) {
console.log('SET '+propKey+'='+value);
propData[propKey] = value;
}
})
});
return obj
}

注意,我们正在破坏性地更改原始实现,这意味着我们是元编程。

在 ES 6 中,我们可以使用基于 Proxy 的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function tracePropAccess(obj, propKeys) {
const propKeySet = new Set(propKeys)
return new Proxy(obj, {
get(target, propKey, receiver) {
if (propKeySet.has(propKey)) {
console.log('GET '+propKey);
}
return Reflect.get(obj, propKey, receiver)
},
set(target, propKey, value, receiver) {
if (propKeySet.has(propKey)) {
console.log('SET '+propKey+'='+value);
}
return Reflect.set(obj, propKey, value, receiver)
}
})
}

可以看到,我们只是拦截了 get 和 set 方法,并通过 Reflect 将方法转发给 obj 自身的调用,没有去改变 obj 原生方法的调用。

关于未知的警告(get, set)

当涉及到访问属性时,JavaScritp 是比较包容的。例如,当我们访问一个拼写错误或者不存在的属性时,程序将不会抛出异常,而只是返回一个 undefined。因此如果,我们想要到达抛出异常的效果,不妨使用 Proxy 来实现。

1
2
3
4
5
6
7
8
const PropertyChecker = new Proxy({}, {
get(target, propKey, receiver) {
if (!(propKey in target)) {
throw new ReferenceError('Unknown property: ' + propKey);
}
return Reflect.get(target, propKey, receiver);
}
});

让我们使用 PropertyChecker 创建一个对象:

1
2
3
4
5
6
const obj = { 
__proto__: PropertyChecker,
foo: 123
};
obj.foo // 123
obj.fo // ReferenceError: Unknown property: fo

如果我们把 PropertyChecker 编程一个构造函数,那么我们可以在 ES 6 通过继承的方式在对象中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const proxy = new Proxy({}, {
get(target, propKey, receiver) {
if (!(propKey in target)) {
throw new ReferenceError('Unknown property: ' + propKey);
}
return Reflect.get(target, propKey, receiver);
}
});
function PropertyChecker() {}
PropertyChecker.prototype = proxy

class Point extends PropertyChecker {
constructor(x, y) {
super()
this.x = x;
this.y = y;
}
}
const point = new Point(4, 6)
console.log(point.x) // 4
console.log(point.z)
// ReferenceError: Unknown property: z

如果你担心对象属性被以外修改,有两个选择:

  • 使用 Proxy 的陷阱方法 set 对对象进行包装拦截
  • 使用 Object.preventExtensions(obj) 来显示的设置 obj 为不可扩展

数组的负值索引

一些数组方法允许通过 -1 引用最后一个元素,通过 -2 引用倒数第二个元素,等等,例如:

1
2
> ['a', 'b', 'c'].slice(-1)
[ 'c' ]

但是,当我们直接使用方括号([])语法访问数组下标时,就没办法达到上述效果了。那么,我们可以使用 Proxy 来给数组添加这个能力。下面的 createArray() 创建的数组将支持负索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createArray(...elements) {
const handler = {
get(target, propKey, receiver) {
const index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
const target = [];
target.push(...elements);
return new Proxy(target, handler);
}
const arr = createArray('a', 'b', 'c');
console.log(arr[-1]); // c

数据绑定(set)

数据绑定是一种对象之间数据保持同步的方式。一个流行的用例是基于 MVC 模式的视图数据更新: 使用数据绑定,如果更改模型Model(视图可视化的数据),视图将自动更新数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createObserverArray(callback) {
const array = []
return new Proxy(array, {
set(target, propKey, value, receiver) {
callback(propKey, value);
return Reflect.set(target, propKey, value, receiver);
}
})
}
const observedArray = createObserverArray(
(key, value) => console.log(`arr[ ${key} ] = ${value}`));
observedArray.push('a');
observedArray.push('b');
// arr[ 0 ] = a
// arr[ length ] = 1
// arr[ 1 ] = b
// arr[ length ] = 2

访问基于 rest 风格的 web 服务

可以使用代理创建一个对象,在该对象上可以调用任意方法。在下面的示例中,函数 createWebService 就创建了这样的 service 对象。调用 service 上的方法检索具有相同名称的web服务资源的内容。检索结果是通过 ES 6 的 Promise 来处理的

1
2
3
4
5
const service = createWebService('http://example.com/data');
service.employees().then(json => {
const employees = JSON.parse(json);
···
});

下面我们看下相对复杂的 ES 5 的实现方式,在没有使用代理的情况下,为了事先定义好调用方法,我们需要传入一个包含调用方法名的 propKeys 数组,以保证 createWebService 创建出来的对象是存在调用的方法的。

1
2
3
4
5
6
7
8
9
function createWebService(baseUrl, propKeys) {
const service = {};
propKeys.forEach(function (propKey) {
service[propKey] = function () {
return httpGet(baseUrl + '/' + propKey);
};
});
return service;
}

那么在 ES 6 中,我们通过使用 Proxy 来使实现变得更加简单(因为拦截了 get 方法的缘故,我们无需关心对象调用的方法是否存在,都将将会执行相应请求):

1
2
3
4
5
6
7
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return () => httpGet(baseUrl + '/' + propKey);
}
});
}

这两个实现都使用以下函数来发出 HTTP GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function httpGet(url) {
return new Promise(
(resolve, reject) => {
const request = new XMLHttpRequest();
Object.assign(request, {
onload() {
if (this.status === 200) {
// Success
resolve(this.response);
} else {
// Something went wrong (404 etc.)
reject(new Error(this.statusText));
}
},
onerror() {
reject(new Error(
'XMLHttpRequest Error: ' + this.statusText));
}
});
request.open('GET', url);
request.send();
});
}

可撤销引用

可撤销引用的工作方式如下:

客户端不允许直接访问重要资源(对象),只能通过引用(中间对象,资源的包装器)。通常,作用于引用的每个操作都被转发到资源。客户端完成后,通过撤销引用(通过关闭引用)来保护资源。从此以后,对引用执行操作将抛出异常,而不再转发任何内容。

下面例子中,我们为资源创建一个可撤销的引用。然后,通过引用读取资源的一个属性。是能够正常访问到的,因为这时的引用是允许我们访问的。接下来,我们撤销(revoke)引用。现在引用不再让我们访问属性了。

1
2
3
4
5
6
7
const resource = { x: 11, y: 8 }
const { refrence, revoke } = createRevocableReference(resource);
// 允许访问
console.log(refrence.x);
revoke();
// 禁止访问
console.log(refrence.x);

Proxy非常适合实现可撤销引用,因为它可以拦截和转发操作。这是createRevocableReference的一个简单的基于代理的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createRevocableReference(target) {
let enabled = true;
return {
refrence: new Proxy(target, {
get(target, propKey, receiver) {
if (!enabled) {
throw new TypeError('Revoked');
}
return Reflect.get(target, propKey, receiver);
},
has(target, propKey) {
if (!enabled) {
throw new TypeError('Revoked');
}
return Reflect.has(target, propKey)
}
// ...
}),
revoke() {
enabled = false
}
}
}

可以通过前一节中的代理处理程序(proxy-as-handler)技术简化代码。这一次,处理程序基本上是 Reflect 对象。因此,get 陷阱通常返回相应的 Reflect 方法。如果引用已被撤销,则会抛出类型错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createRevocableReference2(target) {
let enabled = true;
const handler = new Proxy({}, {
get(dummyTarget, trapName, receiver) {
if (!enabled) {
throw new TypeError('Revoked')
}
console.log(trapName)
return Reflect[trapName]
}
})
return {
refrence: new Proxy(target, handler),
revoke() {
enabled = false;
}
}
}

然而,我们不必自己实现可撤销引用,因为ECMAScript 6允许我们创建可撤销(revocable)的proxy。这一次,撤销发生在代理proxy中,而不是在处理程序handler中。处理程序所要做的就是将每个操作转发给目标。正如我们所看到的,如果处理程序不实现任何陷阱,就会自动发生这种情况。

1
2
3
4
5
6
7
8
function createRevocableReference3(target) {
const handler = {}; // 转发所有的方法
const { proxy, revoke } = Proxy.revocable(target, handler);
return {
refrence: proxy,
revoke
}
}

Proxy 的全部API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface ProxyHandler<T extends object> {
getPrototypeOf? (target: T): object | null;
setPrototypeOf? (target: T, v: any): boolean;
isExtensible? (target: T): boolean;
preventExtensions? (target: T): boolean;
getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
has? (target: T, p: PropertyKey): boolean;
get? (target: T, p: PropertyKey, receiver: any): any;
set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
deleteProperty? (target: T, p: PropertyKey): boolean;
defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
enumerate? (target: T): PropertyKey[];
ownKeys? (target: T): PropertyKey[];
apply? (target: T, thisArg: any, argArray?: any): any;
construct? (target: T, argArray: any, newTarget?: any): object;
}

interface ProxyConstructor {
revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; };
new <T extends object>(target: T, handler: ProxyHandler<T>): T;
}
declare var Proxy: ProxyConstructor;

操作系统-陷阱

发表于 2018-10-03 | 分类于 system

维基百科

在计算机中,陷阱也称为异常或故障,通常是一种由异常条件(如断点、零除、无效内存访问)引起的同步中断。陷阱通常会导致切换到内核模式,操作系统将执行一些操作,然后再返回到初始进程。通常来讲,陷阱是专门指用于启动到监视器程序或调试器的上下文切换的中断。

一般性解读

陷阱 是一种软件中断,由代码调用(如应用程序),调用操作系统提供的接口(提供操作硬件的指令,通常是同步的)。中断由事件调用(很多时候是硬件,比如接收数据的网卡或CPU定时器)引起,顾名思义中断操作系统正常的控制流,因为CPU必须切换到驱动程序来处理事件。

陷阱的实际作用

在计算机中,存在两种状态:用户态和内核态。

  • 内核态

    操作系统运行时处于内核态中,也可以理解为操作系统就是内核,其对计算机的所有硬件有着安全访问的权限,可以使机器运行任何指令。

  • 用户态

    计算机的应用程序,运行时处于用户态,应用程序不具备直接操作硬件的权限。

那么,我们可能会想,当我执行一个播放音乐的软件时,点击播放时,计算机开始播放音乐,这不就是对硬件中的声卡进行操作的吗?

我们的疑问其实就是陷阱的作用,陷阱 可以使应用程序由用户态陷入内核态,把控制转交给操作系统,使得应用程序可以调用内核函数和使用硬件,从而间接的获得操作系统操作硬件的权限。

如我们播放音乐时,通过调用内核(操作系统)提供的接口,来告诉操作系统我们要播放音频了,那么操作系统就知道它接下来该怎么做了,最终我们的音乐按照我们的操作顺利的播放了。

12
jclee

jclee

study study and study

11 日志
5 分类
3 标签
© 2019 jclee
由 Hexo 强力驱动
主题 - NexT.Pisces