欢迎来到feenan的藏宝洞!这里是我的第三个宝贝哦。如果讲的不对,当(huan)我(ying)没(zhi)说(zheng)。

某日在完成当日掘金任务的时候,刷到了一个帖子,帖子内容是关于面试中被问到如何实现一个无法访问到window,document,fetch的一个隔离环境。本人第一时间想到了属性描述符(懂的人先勿喷)。写到这里感觉自己是真的不会水文呀,想当年我写小说的时候,也是该水的时候不会水,不该水的时候,主角们的一个照面都能写800字,这么多年竟毫无长进。

如果你还不知道属性描述符的话,那么我们先浅介绍一下,

一.属性描述符

  roperty Descriptor(属性描述符) 是一个普通对象,用于描述一个属性的相关信息。我们可以通过Object.getOwnPropertyDescriptor(对象, 属性名);就可以得到一个对象的某个属性的属性描述符。
返回的属性描述符对象有以下几个属性:

  • value:属性值
  • configurable:该属性的描述符是否可以修改
  • enumerable:该属性是否可以被枚举
  • writable:该属性是否可以被重新赋值
    例如:

这些属性各有各的妙用,但由于男主角不是属性描述符,在此就不多做赘述了。
重点来了,我们可以使用Object.defineProperty(被定义属性的对象,属性名,属性描述符)来给一个对象新增属性或者修改属性。

二.属性描述符能帮助我们实现沙盒吗?

  先来试试属性描述符能否帮助我们直接实现一个简易的无法获取document的沙盒。

1
2
3
Object.defineProperty(window, 'document', { get: ()=>{
throw new Error('document无法访问')
} })

  很好,首战失利。
  经过我的调查和咨询,好吧其实就是百度了一下,发现原来一个属性的属性描述符中configurable如果为false,那么是无法被重新修改属性描述符的,这就代表了其value也是无法被修改的,所以那些想要通过对这些变量重新赋值undefined来达到目的的童鞋也可以歇歇了,到了这一步我意识到,这也是为什么我们用[Var]声明了一个变量xxx后,无法通过 delete window.xxx 来删除xxx变量的原因了吧。

既然属性描述符无法达到我们的目的,那还有谁能来拯救我们于面试官的扎心拷问中呢???还是我。

三.with

  with语句,一个我从来没用过,只是两年前入行的时候学到过的一个关键字,依稀记得这玩意儿能改变作用域链,赶忙回头翻笔记。不得不提一句,做笔记真的是好习惯,我不相信人脑的记忆力,以至于我的电脑里有我的所有笔记,工作后也会写工作记录文档。“让一切可追溯。”是我从我入职的第一家公司学习到的好习惯。不过,过于依赖外力,也导致我越来越懒,,,,
言归正传,with语句的作用是什么呢?
   改变作用域链:使得其后花括号里的内容最直接的AO是传入的对象。在一个函数里使用就使得优先级发生了变化,从函数自己的AO到GO变成了从obj到AO到GO。通俗易懂的来讲就是:with语句中对于任何变量的访问,先在传入的obj里找,找到就拿,找不到就继续沿着作用域链找(非函数块里的with,就会直接在全局找,如果是函数块里的with,就会先在函数块级作用域找),直到拿到。下面是一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
x:'1',
};
const x = '3';
const y = '4';
with(obj){
function s(){
console.log(x); // 1
console.log(y); // 4
}
s();
console.log(x); //1
}

但是我们只有with还不够,因为如果在传入对象里找不到document的话,也会继续沿着作用域链找,最后仍然可以拿到。所以又有一个久远到我快要忘记的东西,也是今天的女主角 – Proxy。

四.with + proxy代理实现一个简单沙箱

  讲proxy之前,先来讲(mai nong)一下Reflect,Reflect是一个内置的JS对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些JS底层功能。由于它类似于其他语言的反射,因此取名为Reflect。使用Reflect可以实现诸如:属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在与对象中等等功能。
  有些童鞋会有疑问了,这些功能不是已经存在了吗?为什么还需要用Reflect实现一次?有一个重要的理念,在ES5就被提出:减少魔法、让代码更加纯粹。这种理念很大程度上是受到函数式编程的影响,ES6进一步贯彻了这种理念,它认为,对属性内存的控制、原型链的修改、函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取出来,形成一个正常的API,并高度聚合到某个对象中,于是,就造就了Reflect对象。该对象中有很多方法,比如:

  • Reflect.set(target, propertyKey, value): 设置对象target的属性propertyKey的值为value,等同于给对象的属性赋值。
  • Reflect.get(target, propertyKey): 读取对象target的属性propertyKey,等同于读取对象的属性值。
  • Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数,并绑定this和参数列表。等同于函数调用。
  • Reflect.has(target, propertyKey): 判断一个对象是否拥有一个属性
    等等等等。

  而Proxy的厉害之处在于能够重写反射Reflect里的APi。代理修改了底层实现的方式,通过代理对象Proxy来对原变量进行操作。其能够改写的底层实现,就是Reflect里的那些方法属性,并且还能够在里面调用Reflect的方法。
那我们这就正式开始构建沙箱吧!

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
// 不可访问全局作用域的黑名单列表
const unaccess_white_list = ["window", "document", 'XMLHttpRequest', 'fetch'];

// 封装一个工具类
class SandboxGlobalProxy {
constructor(objO, unaccess_white_list) {
return new Proxy(objO, {
has: (target, prop) => {
if (unaccess_white_list.includes(prop) && !target.hasOwnProperty(prop)) {
// 如果属性在黑名单里,且该对象里没有的话就抛出错误
throw new Error(`Not find: ${prop}!`)
}
// 如果没有该属性,沿作用域链向外找
if (!target.hasOwnProperty(prop)) {
return false;
}
// 属性存在,返回objO中的值
return true;
}
});
}
}
const obj2 = {
name: '李白喵',
callBack: function(){console.log(name)}
}
const name = '小石头要变成星球';
const age = '18';
const obj2Proxy = new SandboxGlobalProxy(obj2, unaccess_white_list);
with(obj2Proxy){
console.log(name); // 李白喵
console.log(age); // 18
callBack();
console.log(document); // Uncaught Error: Not find: document!
}

这就实现了一个简易沙箱了【耶耶耶】,你有什么其他方案吗?

五.本期疑问

在上文采用with和proxy实现简易沙箱的过程中,我们在with语句中调用了callBack方法,大家猜一猜,打印出来的会是什么呢?下期见。