Mooory

proxy和reflect实现数据拦截处理

最近在面试时,遇到双向绑定的原理,我只了解脏检查和object.defineProperty进行数据挟持的方法。面试官问我还知道别的方法吗?(what?)不造啊

后来私下查了一下,es6的proxy可对数据进行一层“拦截”,同样能够实现对数据的一些操作:比如双向绑定。

proxy

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以是一种“元编程”,即对编程语言进行编程。

简单地理解,就是在目标对象之前假设一层“拦截”,外界对改对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤。基本就想字面意思:代理。

1
var proxy = new Proxy(target, handler);

其中,target、handler 均为对象,target 指目标对象(需要拦截的),handler 中定义拦截行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`); // es 6 新増的字符串拓展写法
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});

obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2

上面重新定义了属性的读取和设置,搭配Reflect一起使用。

注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。

Proxy 实例也可以作为其他对象的原型对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});

let obj = Object.create(proxy);
/*
Object.create()方法接受两个参数:Object.create(obj,propertiesObject) ;
obj:一个对象,应该是新创建的对象的原型。
propertiesObject:可选。该参数对象是一组属性与值,该对象的属性名称将是新创建的对象的属性名称,值是属性描述符(这些属性描述符的结构与Object.defineProperties()的第二个参数一样)。注意:该参数对象不能是 undefined,另外只有该对象中自身拥有的可枚举的属性才有效,也就是说该对象的原型链上属性是无效的。
*/
obj.time // 35

proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。

拦截器上可以设置多个操作,如果没有设置,直接落在目标对象上,按原先的方法产生结果。

操作

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//get
var person = {
name: "张三"
};

var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});

proxy.name // "张三"
proxy.age // 抛出一个错误

this问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

1
2
3
4
5
6
7
8
9
10
11
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m() // true

reflect

Reflect对象的设计目的有这样几个。

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}

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

1
2
3
4
5
// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true

(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

这就是前面使用的功能,使用reflect完成默认行为。保证原生行为能够正常执行。

Continue
浅谈发布/订阅模式

观察者模式时软件设计模式的一种。
在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

Continue
常见排序算法的原理与实现

简介

本文介绍了冒泡、选择、插入、归并、快速、堆排序等多种排序的原理、js实现和性能表现。希望能把排序算法发光发热!

  • 冒泡: 将数组相邻元素两两比较大小,一遍一遍把当前最大值(最小值)冒泡到最后。
  • 选择: 通过数组循环,记录当前最小值(最大值)所在,再与第一个元素进行元素交换,依次类推。
  • 插入: 对未排序数据中,从已排序序列中从后向前查询,找到相应的位置进行插入。
  • 归并: 将数组拆分成n个一个元素的数组,再进行两两合并。从底层往上合并时,左右两边的数组都是排序好的,因此合并容易。
  • 快排: 选择基准元素(一般是中间),所有小于基准的放在左边,大于基准的放在右边,重复直到结束。
  • 堆排: 初始化堆后,将最后一个元素与堆顶元素交换后,最后元素移出堆排序序列中,再不断调整堆使满足堆的性质。
Continue
求取两个排序数组的中位数

##问题:

给定两个大小为 m 和 n 的有序数组 nums1 和 nums2 。
请找出这两个有序数组的中位数。要求算法的时间复杂度为 O(log (m+n)) 。
你可以假设 nums1 和 nums2 不同时为空。
示例 1:
nums1 = [1, 3]
nums2 = [2]
中位数是 2.0
示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
中位数是 (2 + 3)/2 = 2.5

##思考:
总体思考:
最主要的难点是算法复杂度的限制, log级别的时间复杂度我们自然想到二分法,每次取中间值,自然得出的是log2(n)级别的。
因为nums1和nums2是有序数组,我们对这两个数组分别在某一位置进行划分。

比如nums1数组设为A, 划分位置为 i :
left_A | right_A
A [0],A [1],…,A [i-1] | A [i],A [i + 1],…,A [m-1]
这样左边有 i 个元素,右边有( m - i )个元素。

nums2数组设为B,划分位置为 j :
left_B | right_B
B [0],B [1],…,B [j-1] | B [j],B [j + 1],…,B [n-1]
这样左边有 j 个元素,右边有( n - j )个元素。

把两个数组的左右两边分别混合,也就是left_A和left_B放一起,right_A和right_B放一起,得到中位数需要满足下面两个条件:

  1. 左右数量相等(偶数:i + j = m - i + n -j )或者左边比右边大一个(奇数:i + j = m - i + n -j + 1)
  2. max(left_A) 小于 min(right_B),而且max(left_B) 小于 min(right_A),因为left_A肯定小于right_A, left_B肯定小于right_B

那么对于总长度为奇数,中位数 = max(left_A, left_B);对于总长度为偶数,中位数 = ( max(left_A, left_B) ,min(right_A,right_B) ) / 2

把上面两个条件进行整理:

  1. i = 0〜m,j = parseInt( (m + n + 1) / 2) - i (奇偶通用,阴影划分的为左边,其他为右边)
    总长度为奇数
    总长度为偶数
    所以n要大于等于m(n>=m),这样确保 j 是合法索引。
  2. shortArray[ i -1] <= longArray[ j ],longArray[ j - 1 ] <= shortArray [ i ]

##实现细节:

  1. 先对nums1和nums2的长度进行判断,选择长的作为longArray, 短的作为shortArray
  2. i 在 [ 0, m ]中进行取值,并根据二分法取中值,j = parseInt( (m + n + 1) / 2) - i
  3. 如果( j > 0 && i < m && longArray[j-1] > shortArray[i] ),说明shortArray [i]太小,i 应该继续增大,取值范围变为 [ i + 1, m ]
  4. 如果( i > 0 && j < n && shortArray[i-1] > longArray[j] ),说明shortArray [i]太大, i 应该减小,取值范围变为[0, i - 1]
  5. 以此循环,直到上面条件都不满足,说明要么取到边界了(i =0 , j=0 等情况),要么shortArray和longArray左边都取了值,要选出shortArray和longArray左边最大的值。
  • (i =0 情况下,左侧确定,左边最大值为longArray[j - 1] )
    image.png
  • (j = 0情况下,左侧确定,左侧最大值为shortArray[i - 1])
    image.png
  • (其他情况,shortArray和longArray左边都取了值)
    image.png
  1. 对于总长度为奇数, 中位数 = max(left_A, left_B), 也就是第5步得到的值。如果是奇数,要再得到 minRight.

##代码:

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
var findMedianSortedArrays = function(nums1, nums2) {
var shortArray = [],longArray = [], m = nums1.length, n = nums2.length,
temp = 0, maxLeft = 0, minRight = 0;
//保证 n >= m
if(m > n) {
shortArray = nums2;
longArray = nums1;
temp = m;
m = n;
n = temp;
} else {
shortArray = nums1;
longArray = nums2;
}

//假设shortArray取i个,longArray取parseInt((m+n+1)/2)-i个
var imin = 0, imax = m, i, j;
do {
i = parseInt((imin + imax)/2);
j = parseInt((m + n + 1)/2 - i);
if(j > 0 && i < m && longArray[j-1] > shortArray[i]) {
// shortArray和longArray左右都取了值,且i值取小了
imin = i + 1;
} else if( i > 0 && j < n && shortArray[i-1] > longArray[j]) {
// shortArray和longArray左右都取了值,且i值取大了
imax = i - 1;
} else {
// 考虑边界情况
if(i == 0) {
maxLeft = longArray[j - 1];
} else if (j == 0 ) {
maxLeft = shortArray[i - 1];
} else {
// i值不小不大, 也就是(shortArray[i-1] <= longArray[j] && longArray[j-1] <= shortArray[i])
maxLeft = Math.max(shortArray[i - 1], longArray[j -1]);
}
break;
}
} while (imin <= imax);
//两数组相加个数是奇数,只要取中间值
if( (m + n)%2 == 1 ) return maxLeft;
//个数是偶数,必须左右两边中位数相加除以二
if(i == m) {
minRight = longArray[j];
} else if(j == n){
minRight = shortArray[i];
} else {
minRight = Math.min(shortArray[i], longArray[j]);
};
return (maxLeft + minRight)/2;
}

Continue
flux/redux/mobx/vuex相关解析

状态管理是什么?

在组件化开发的新前端发展史上,组件的发展提供了更好的编码效率,更好的代码阅读性,维护性,补充HTML5语义化标签的不足。前端承担了越来越多的任务,特别是做一个spa项目。然而我们父子组件沟通可以通过props和回调,但是在两个组件我们并不知道它们的调用关系的时候,如何进行沟通呢?

Continue
node第一步!

node在我们前端运用中频繁被提及,现在让我们来了解node是什么!

简介

Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。
作为异步驱动的 JavaScript 运行时,Node 被设计成可升级的网络应用。
Node 的用户不必担心死锁过程, 因为没有锁。Node 中几乎没有函数直接执行 I/O 操作,因此进程从不阻塞。由于没有任何阻塞,可伸缩系统在 Node 中开发是非常合理的。

Continue
webpack原理呀!

本文将简单介绍webpack相关的原理~

webpack构建流程

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader
  5. 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  6. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  7. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  8. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

Continue
Home Archives Tags About Search
Theme Nayo