咱们已经讨论过了前端与计算机基础的不少话题,诸如SQL、面向对象、多线程,本篇将讨论数据结构与算法,以我接触过的一些例子作为说明。javascript
递归就是本身调本身,递归在前端里面算是一种比较经常使用的算法。假设如今有一堆数据要处理,要实现上一次请求完成了,才能去调下一个请求。一个是能够用Promise,就像《前端与SQL》这篇文章里面提到的。可是有时候并不想引入Promise,能简单处理先简单处理。这个时候就能够用递归,以下代码所示:css
var ids = [34112, 98325, 68125]; (function sendRequest(){ var id = ids.shift(); if(id){ $.ajax({url: "/get", data: {id}}).always(function(){ //do sth. console.log("finished"); sendRequest(); }); } else { console.log("finished"); } })(); 复制代码
上面代码定义了一个sendRequest的函数,在请求完成以后再调一下本身。每次调以前先取一个数据,若是数组已经为空,则说明处理完了。这样就用简单的方式实现了串行请求不堵塞的功能。html
再来说另一个场景:DOM树。前端
因为DOM是一棵树,而树的定义自己就是用的递归定义,因此用递归的方法处理树,会很是地简单天然。例如用递归实现一个查DOM的功能document.getElementById。java
function getElementById(node, id){ if(!node) return null; if(node.id === id) return node; for(var i = 0; i < node.childNodes.length; i++){ var found = getElementById(node.childNodes[i], id); if(found) return found; } return null; } getElementById(document, "d-cal");复制代码
document是DOM树的根结点,通常从document开始往下找。在for循环里面先找document的全部子结点,对全部子结点递归查找他们的子结点,一层一层地往下查找。若是已经到了叶子结点了尚未找到,则在第二行代码的判断里面返回null,返回以后for循环的i加1,继续下一个子结点。若是当前结点的id符合查找条件,则一层层地返回。因此这是一个深度优先的遍历,每次都先从根结点一直往下直到叶子结点,再从下往上返回。node
最后在控制台验证一下,执行结果以下图所示:git
使用递归的优势是代码简单易懂,缺点是效率比不上非递归的实现。Chrome浏览器的查DOM是使用非递归实现。非递归要怎么实现呢?github
以下代码:web
function getByElementId(node, id){ //遍历全部的Node while(node){ if(node.id === id) return node; node = nextElement(node); } return null; }复制代码
仍是依次遍历全部的DOM结点,只是这一次改为一个while循环,函数nextElement负责找到下一个结点。因此关键在于这个nextElement如何非递归实现,以下代码所示:ajax
function nextElement(node){ if(node.children.length) { return node.children[0]; } if(node.nextElementSibling){ return node.nextElementSibling; } while(node.parentNode){ if(node.parentNode.nextElementSibling) { return node.parentNode.nextElementSibling; } node = node.parentNode; } return null; }复制代码
仍是用深度遍历,先找当前结点的子结点,若是它有子结点,则下一个元素就是它的第一个子结点,不然判断它是否有相邻元素,若是有则返回它的下一个相邻元素。若是它既没有子结点,也没有下一个相邻元素,则要往上返回它的父结点的下一个相邻元素,至关于上面递归实现里面的for循环的i加1.
在控制台里面运行这段代码,一样也能够正确地输出结果。无论是非递归仍是递归,它们都是深度优先遍历,这个过程以下图所示。
实际上getElementById浏览器是用的一个哈希map存储的,根据id直接映射到DOM结点,而getElementsByClassName就是用的这样的非递归查找。
上面是单个选择器的查找,按id,按class等,多个选择器应该如何查找呢?
如实现一个document.querySelector:
document.querySelector(".mls-info > div .copyright-content")复制代码
首先把复杂选择器作一个解析,序列为如下格式:
//把selector解析为 var selectors = [ {relation: "descendant", matchType: "class", value: "copyright-content"}, {relation: "child", matchType: "tag", value: "div"}, {relation: "subSelector", matchType: "class", value: "mls-info"}];复制代码
从右往左,第一个selector是.copyright-content
,它是一个类选择器,因此它的matchType是class,它和第二个选择器是祖先和子孙关系,所以它的relation是descendant;同理第二个选择器的matchType是tag,而relation是child,表示是第三个选择器的直接子结点;第三个选择器也是class,可是它没有下一个选择器了,relation用subSelector表示。
matchType的做用就在于用来比较当前选择器是否match,以下代码所示:
function match(node, selector){ if(node === document) return false; switch(selector.matchType){ //若是是类选择器 case "class": return node.className.trim().split(/ +/).indexOf(selector.value) >= 0; //若是是标签选择器 case "tag": return node.tagName.toLowerCase() === selector.value. toLowerCase(); default: throw "unknown selector match type"; } }复制代码
根据不一样的matchType作不一样的匹配。
在匹配的时候,从右往左,依次比较每一个选择器是否match. 在比较下一个选择器的时候,须要找到相应的DOM结点,若是当前选择器是下一个选择器的子孙时,则须要比较当前选择器全部的祖先结点,一直往上直到document;而若是是直接子元素的关系,则比较它的父结点便可。因此须要有一个找到下一个目标结点的函数:
function nextTarget(node, selector){ if(!node || node === document) return null; switch(selector.relation){ case "descendant": return {node: node.parentNode, hasNext: true}; case "child": return {node: node.parentNode, hasNext: false}; case "sibling": return {node: node.previousSibling, hasNext: true}; default: throw "unknown selector relation type"; //hasNext表示当前选择器relation是否容许继续找下一个节点 } }复制代码
有了nextTarge和match这两个函数就能够开始遍历DOM,以下代码所示:
最外层的while循环和简单选择器同样,都是要遍历全部DOM结点。对于每一个结点,先判断第一个选择器是否match,若是不match的话,则继续下一个结点,若是不是标签选择器,对于绝大多数结点将会在这里判断不经过。若是第一个选择器match了,则根据第一个选择器的relation,找到下一个target,判断下一个targe是否match下一个selector,只要有一个target匹配上了,则退出里层的while循环,继续下一个选择器,若是全部的selector都能匹配上说明匹配成功。若是有一个selecotr的全部target都没有match,则说明匹配失败,退出selector的for循环,直接从头开始对下一个DOM结点进行匹配。
这样就实现了一个复杂选择器的查DOM。写这个的目的并非要你本身写一个查DOM的函数拿去用,而是要明白查DOM的过程是怎么样的,能够怎么实现,浏览器又是怎么实现的。还有能够怎么遍历DOM树,当明白这个过程的时候,遇到相似的问题,就能够触类旁通。
最后在浏览器上运行一下,以下图所示:
如今有个问题,以下图所示:
当地图往下拖的时候要更新地图上的房源标签数据,上图绿框表示不变的标签,而黄框表示新加的房源。
后端每次都会把当前地图可见区域的房源返回给我,当用户拖动的时候须要知道哪些是原先已经有的房源,哪些是新加的。把新加的房源画上,而把超出区域的房源删掉,已有的房源保持不动。所以须要对比当前房源和新的结果哪些是重复的。由于若是不这样作的话,改为每次都是所有删掉再从新画,已有的房源标签就会闪一下。所以为了不闪动作一个增量更新。
把这个问题抽象一下就变成:给两个数组,须要找出第一个数组里面的重复值和非重复值。即有一个数组保存上一次状态的房源,而另外一个数组是当前状态的新房源数据。找到的重复值是须要保留,找到非重复值是要删掉的。
最直观的方法是使用双重循环。
以下代码所示:
var lastHouses = []; filterHouse: function(houses){ if(lastHouses === null){ lastHouses = houses; return { remainsHouses: [], newHouses: houses }; } var remainsHouses = [], newHouses = []; for(var i = 0; i < houses.length; i++){ var isNewHouse = true; for(var j = 0; j < lastHouses.length; j++){ if(houses[i].id === lastHouses[j].id){ isNewHouse = false; remainsHouses.push(lastHouses[j]); break; } } if(isNewHouse){ newHouses.push(houses[i]); } } lastHouses = remainsHouses.concat(newHouses); return { remainsHouses: remainsHouses, newHouses: newHouses }; } 复制代码
上面代码有一个双重for循环,对新数据的每一个元素,判断老数据里面是否已经有了,若是有的话则说明是重复值,若是老数据循环了一遍都没找到,则说明是新数据。因为用到了双重循环,因此这个算法的时间复杂度为O(N2),对于百级的数据还好,对于千级的数据可能会有压力,由于最坏状况下要比较1000000次。
以下代码所示:
var lastHouses = new Set(); function filterHouse(houses){ var remainsHouses = [], newHouses = []; for(var i = houses.length - 1; i >= 0; i--){ if(lastHouses.has(houses[i].id)){ remainsHouses.push(houses[i]); } else { newHouses.push(houses[i]); } } for(var i = 0; i < newHouses.length; i++){ lastHouses.add(newHouses[i].id); } return {remainsHouses: remainsHouses, newHouses: newHouses}; }复制代码
老数据的存储lastHouses从数组改为set,但若是一开始就是数组呢,就像问题抽象里面说的给两个数组?那就用这个数组的数据初始化一个Set.
使用Set和使用Array的区别在于能够减小一重循环,调用Set.prototype.has的函数。Set通常是使用红黑树实现的,红黑树是一种平衡查找二叉树,它的查找时间复杂度为O(logN)。因此时间上进行了改进,从O(N)变成O(logN),而整体时间从O(N2)变成O(NlogN)。实际上,Chrome V8的Set是用哈希实现的,它是一个哈希Set,查找时间复杂度为O(1),因此整体的时间复杂度是O(N).
无论是O(NlogN)仍是O(N),表面上看它们的时间要比O(N2)的少。但实际上须要注意的是它们前面还有一个系数。使用Set在后面更新lastHouses的时候也是须要时间的:
for(var i = 0; i < newHouses.length; i++){ lastHouses.add(newHouses[i].id); }复制代码
若是Set是用树的实现,这段代码是时间复杂度为O(NlogN),因此总的时间为O(2NlogN),可是因为大O是不考虑系数的,O(2NlogN) 仍是等于O(NlogN),当数据量比较小的时侯,这个系数会起到很大的做用,而数据量比较大的时候,指数级增加的O(N2)将会远远超过这个系数,哈希的实现也是一样道理。因此当数据量比较小时,如只有一两百可直接使用双重循环处理便可。
上面的代码有点冗长,咱们能够用ES6的新特性改写一下,变得更加的简洁:
function filterHouse(houses){ var remainsHouses = [], newHouses = []; houses.map(house => lastHouses.has(house.id) ? remainsHouses.push(house) : newHouses.push(house)); newHouses.map(house => lastHouses.add(house.id)); return {remainsHouses, newHouses}; }复制代码
代码从16行变成了8行,减小了一半。
使用Map也是相似的,代码以下所示:
var lastHouses = new Map(); function filterHouse(houses){ var remainsHouses = [], newHouses = []; houses.map(house => lastHouses.has(house.id) ? remainsHouses.push(house) : newHouses.push(house)); newHouses.map(house => lastHouses.set(house.id, house)); return {remainsHouses, newHouses}; }复制代码
哈希的查找复杂度为O(1),所以总的时间复杂度为O(N),Set/Map都是这样,代价是哈希的存储空间一般为数据大小的两倍
最后作下时间比较,为此得先造点数据,比较数据量分别为N = 100, 1000, 10000的时间,有N/2的id是重复的,另一半的id是不同的。用如下代码生成:
var N = 1000; var lastHouses = new Array(N); var newHouses = new Array(N); var data = new Array(N); for(var i = 0; i < N / 2; i++){ var sameNumId = i; lastHouses[i] = newHouses[i] = {id: sameNumId}; } for(; i < N; i++){ lastHouses[i] = {id: N + i}; newHouses[i] = {id: 2 * N + i}; }复制代码
而后须要将重复的数据随机分布,可用如下函数把一个数组的元素随机分布:
//打散 function randomIndex(arr){ for(var i = 0; i < arr.length; i++){ var swapIndex = parseInt(Math.random() * (arr.length - i)) + i; var tmp = arr[i]; arr[i] = arr[swapIndex]; arr[swapIndex] = tmp; } } randomIndex(lastHouses); randomIndex(newHouses);复制代码
Set/Map的数据:
var lastHousesSet = new Set(); for(var i = 0; i < N; i++){ lastHousesSet.add(lastHouses[i].id); } var lastHousesMap = new Map(); for(var i = 0; i < N; i++){ lastHousesMap.set(lastHouses[i].id, lastHouses[i]); }复制代码
分别重复100次,比较时间:
console.time("for time"); for(var i = 0; i < 100; i++){ filterHouse(newHouses); } console.timeEnd("for time"); console.time("Set time"); for(var i = 0; i < 100; i++){ filterHouseSet(newHouses); } console.timeEnd("Set time"); console.time("Map time"); for(var i = 0; i < 100; i++){ filterHouseMap(newHouses); } console.timeEnd("Map time");复制代码
使用Chrome 59,当N = 100时,时间为: for < Set < Map,以下图所示,执行三次:
当N = 1000时,时间为:Set = Map < for,以下图所示:
当N = 10000时,时间为Set = Map << for,以下图所示:
能够看出,Set和Map的时间基本一致,当数据量小时,for时间更少,但数据量多时Set和Map更有优点,由于指数级增加仍是挺恐怖的。这样咱们会有一个问题,究竟Set/Map是怎么实现的。
咱们来研究一下Chrome V8对Set/Map的实现,源码是在chrome/src/v8/src/js/collection.js这个文件里面,因为Chrome一直在更新迭代,因此有可能之后Set/Map的实现会发生改变,咱们来看一下如今是怎么实现的。
以下代码初始化一个Set:
var set = new Set(); //数据为20个数 var data = [3, 62, 38, 42, 14, 4, 14, 33, 56, 20, 21, 63, 49, 41, 10, 14, 24, 59, 49, 29]; for(var i = 0; i < data.length; i++){ set.add(data[i]); } 复制代码
这个Set的数据结构究竟是怎么样的呢,是怎么进行哈希的呢?
哈希的一个关键的地方是哈希算法,即对一堆数或者字符串作哈希运算获得它们的随机值,V8的数字哈希算法是这样的:
function ComputeIntegerHash(key, seed) { var hash = key; hash = hash ^ seed; //seed = 505553720 hash = ~hash + (hash << 15); // hash = (hash << 15) - hash - 1; hash = hash ^ (hash >>> 12); hash = hash + (hash << 2); hash = hash ^ (hash >>> 4); hash = (hash * 2057) | 0; // hash = (hash + (hash << 3)) + (hash << 11); hash = hash ^ (hash >>> 16); return hash & 0x3fffffff; }复制代码
把数字进行各类位运算,获得一个比较随机的数,而后对这个数对行散射,以下所示:
var capacity = 64; var indexes = []; for(var i = 0; i < data.length; i++){ indexes.push(ComputeIntegerHash(data[i], seed) & (capacity - 1)); //去掉高位 } console.log(indexes)复制代码
散射的目的是获得这个数放在数组的哪一个index。
因为有20个数,容量capacity从16开始增加,每次扩大一倍,到64的时候,能够保证capacity > size * 2,由于只有容量是实际存储大小的两倍时,散射结果重复值才能比较低。
计算结果以下:
能够看到散射的结果仍是比较均匀的,可是仍然会有重复值,如14重复了3次。
而后进行查找,例如如今要查找key = 56是否存在这个Set里面,先把56进行哈希,而后散射,按照存放的时候一样的过程:
function SetHas(key){ var index = ComputeIntegerHash(56, seed) & this.capacity; //可能会有重复值,因此须要验证命中的index所存放的key是相等的 return setArray[index] !== null && setArray[index] === key; } 复制代码
上面是哈希存储结构的一个典型实现,可是Chrome的V8的Set/Map并非这样实现的,略有不一样。
哈希算法是同样的,可是散射的时候用来去掉高位的并非用capacity,而是用capacity的一半,叫作buckets的数量,用如下面的data作说明:
var data = [9, 33, 68, 57];复制代码
因为初始化的buckets = 2,计算的结果以下:
因为buckets很小,因此散射值有不少重复的,4个数里面1重复了3次。
如今一个个的插入数据,观察Set数据结构的变化。
以下图所示,Set的存储结构分红三部分,第一部分有3个元素,分别表示有效元素个数、被删除的个数、buckets的数量,前两个数相加就表示总的元素个数,插入9以后,元素个数加1变成1,初始化的buckets数量为2. 第二部分对应buckets,buckets[0]表示第1个bucket所存放的原始数据的index,源码里面叫作entry,9在data这个数组里面的index为0,因此它在bucket的存放值为0,而且bucket的散射值为0,因此bucket[0] = 0. 第三部分是记录key值的空间,9的entry为0,因此它放在了3 + buckets.length + entry * 2 = 5的位置,每一个key值都有两个元素空间,第一个存放key值,第二个是keyChain,它的做用下面将提到。
如今要插入33,因为33的bucket = 1,entry = 1,因此插入后变成这样:
68的bucket值也为1,和33重复了,由于entry = buckets[1] = 1,不为空,说明以前已经存过了,entry为1指向的数组的位置为3 + buckets.length + entry * 2 = 7,也就是说以前的那个数是放在数组7的位置,因此68的相邻元素存放值keyChain为7,同时bucket[1]变成68的entry为2,以下图所示:
插入57也是一样的道理,57的bucket值为1,而bucket[1] = 2,所以57的相邻元素存放3 + 2 + 2 * 2 = 9,指向9的位置,以下图所示:
如今要查找33这个数,经过一样的哈希散射,获得33的bucket = 1,bucket[1] = 3,3指向的index位置为11,可是11放的是57,不是要找的33,因而查看相邻的元素为9,非空,可继续查找,位置9存放的是68,一样不等于33,而相邻的index = 10指向位置7,而7存放的就是33了,经过比较key值相等,因此这个Set里面有33这个数。
这里的数据总共是4个数,可是须要比较的次数比较多,key值就比较了3次,key值的相邻keyChain值比较了2次,总共是5次,比直接来个for循环还要多。因此数据量比较小时,使用哈希存储速度反而更慢,可是当数据量偏大时优点会比较明显。
再继续插入第5个数的时候,发现容量不够了,须要继续扩容,会把容量提高为2倍,bucket数量变成4,再把全部元素再从新进行散射。
Set的散射容量即bucket的值是实际元素的一半,会有大量的散射冲突,可是它的存储空间会比较小。假设元素个数为N,须要用来存储的数组空间为:3 + N / 2 + 2 * N,因此占用的空间仍是挺大的,它用空间换时间。
和Set基本一致,不一样的地方是,map多了存储value的地方,以下代码:
var map = new Map(); map.set(9, "hello");复制代码
生成的数据结构为:
固然它不是直接存的字符串“hello”,而是存放hello的指针地址,指向实际存放hello的内存位置。
JSObject主要也是采用哈希存储,具体我在《从Chrome源码看JS Object的实现》这篇文件章里面已经讨论过。
和JS Map不同的地方是,JSObject的容量是元素个数的两倍,就是上面说的哈希的典型实现。存储结构也不同,有一个专门存放key和一个存放value的数组,若是能找到key,则拿到这个key的index去另一个数组取出value值。当发生散列值冲突时,根据当前的index,直接计算下一个查找位置:
inline static uint32_t FirstProbe(uint32_t hash, uint32_t size) { return hash & (size - 1); } inline static uint32_t NextProbe( uint32_t last, uint32_t number, uint32_t size) { return (last + number) & (size - 1); }复制代码
一样地,查找的时候在下一个位置也是须要比较key值是否相等。
上面讨论的都是数字的哈希,实符串如何作哈希计算呢?
以下所示,依次对字符串的每一个字符的unicode编码作处理:
uint32_t AddCharacterCore(uint32_t running_hash, uint16_t c) { running_hash += c; running_hash += (running_hash << 10); running_hash ^= (running_hash >> 6); return running_hash; } uint32_t running_hash = seed; char *key = "hello"; for(int i = 0; i < strlen(key); i++){ running_hash = AddCharacterCore(running_hash, key[i]); }复制代码
接着讨论一个经典话题
以下,给一个数组,去掉里面的重复值:
var a = [3, 62, 3, 38, 20, 42, 14, 5, 38, 29, 42]; 输出 [3, 62, 38, 20, 42, 14, 5, 29]; 复制代码
以下代码所示:
function uniqueArray(arr){ return Array.from(new Set(arr)); }复制代码
在控制台上运行:
优势:代码简洁,速度快,时间复杂度为O(N)
缺点:须要一个额外的Set和Array的存储空间,空间复杂度为O(N)
以下代码所示:
function uniqueArray(arr){ for(var i = 0; i < arr.length - 1; i++){ for(var j = i + 1; j < arr.length; j++){ if(arr[j] === arr[i]){ arr.splice(j--, 1); } } } return arr; }复制代码
优势:不须要使用额外的存储空间,空间复杂度为O(1)
缺点:须要频繁的内存移动,双重循环,时间复杂度为O(N2)
注意splice删除元素的过程是这样的,这个我在《从Chrome源码看JS Array的实现》已作过详细讨论:
它是用的内存移动,并非写个for循环一个个复制。内存移动的速度仍是很快的,最快1s能够达到30GB,如下图所示:
以下代码所示:
function uniqueArray(arr){ var retArray = []; for(var i = 0; i < arr.length; i++){ if(retArray.indexOf(arr[i]) < 0){ retArray.push(arr[i]); } } return retArray; }复制代码
时间复杂度为O(N2),空间复杂度为O(N)
下面代码是goog.array的去重实现:
和方法三的区别在于,它再也不是使用Array.indexOf判断是否已存在,而是使用Object[key]进行哈希查找,因此它的时间复杂度为O(N),空间复杂为O(N).
最后作一个执行时间比较,对N = 100/1000/10000,分别重复1000次,获得下面的表格:
Object + Array最省时间,splice的方式最耗时(它比较省空间),Set + Array的简洁方式在数据量大的时候时间将明显少于须要O(N2)的Array,一样是O(N2)的splice和Array,Array的时间要小于常常内存移动操做的splice。
实际编码过程当中一、二、4都是能够可取的
方法1 一行代码就能够搞定
方法2 能够用来添加一个Array.prototype.unique的函数
方法4 适用于数据量偏大的状况
上面已经讨论了哈希的数据结构,再来讨论下栈和堆
栈的特色是先进后出,只有push和pop两个函数能够操做栈,分别进行压栈和弹栈,还有top函数查看栈顶元素。栈的一个典型应用是作开闭符号的处理,如构建DOM。有如下html:
<html> <head></head> <body> <div>hello, world</div> <p>goodbye, world</p> </body> </html>复制代码
将会构建这么一个DOM:
上图省略了document结点,而且这里咱们只关心DOM父子结点关系,省略掉兄弟节点关系。
首先把html序列化成一个个的标签,以下所示:
1 html ( 2 head ( 3 head ) 4 body ( 5 div ( 6 text 7 div ) 8 p ( 9 text 10 p ) 11 body ) 12 html)
其中左括号表示开标签,右括号表示闭标签。
以下图所示,处理html和head标签时,它们都是开标签,因此把它们都压到栈里面去,并实例一个HTMLHtmlElement和HTMLHeadElement对象。处理head标签时,因为栈顶元素是html,因此head的父元素就是html。
处理剩余其它元素以下图所示:
遇到第三个标签是head的闭标签,因而进行弹栈,把head标签弹出来,栈顶元素变成了html,因此在遇到第一个标签body的时候,html元素就是body标签的父结点。其它节点相似处理。
上面的过程,我在《从Chrome源码看浏览器如何构建DOM树》已经作过讨论,这里用图表示意,可能会更加直观。
函数执行的时候会把局部变量压到一个栈里面,以下函数:
function foo(){ var a = 5, b = 6, c = a + b; } foo();复制代码
a, b, c三个变量在内存栈的结构以下图所示:
先遇到a把a压到栈里面,而后是b和c,对函数里面的局部变量不断地压栈,内存向低位增加。栈空间大小8MB(可以使用ulimit –a查看),假设一个变量用了8B,一个函数里面定义了10个变量,最多递归8MB / (8B * 10) = 80万次就会发生栈溢出stackoverflow
这个在《WebAssembly与程序编译》这篇文章里面作过讨论。
数据结构里的堆一般是指用数组表示的二叉树,如大堆排序和小堆排序。内存里的堆是指存放new出来动态建立变量的地方,和栈相对,以下图所示:
讨论完了栈和堆,再分析一个比较实用的技巧。
节流是前端常常会遇到的一个问题,就是不想让resize/mousemove/scroll等事件触发得太快,例如说最快每100ms执行一次回调就能够了。以下代码不进行节流,直接兼听resize事件:
$(window).on("resize", adjustSlider);复制代码
因为adjustSlider是一个很是耗时的操做,我并不想让它执行得那么快,最多500ms执行一次就行了。那应该怎么作呢?以下图所示,借助setTimout和一个tId的标志位:
最后再讨论下图像和图形处理相关的。
假设要在前端作一个滤镜,如用户选择了本地的图片以后,点击某个按钮就能够把图片置成灰色的:
效果以下:
一个方法是使用CSS的filter属性,它支持把图片置成灰图的:
img{ filter: grayscale(100%); }复制代码
因为须要把真实的图片数据传给后端,所以须要对图片数据作处理。咱们能够用canvas获取图片的数据,以下代码所示:
<canvas id="my-canvas"></canvas>复制代码
JS处理以下:
var img = new Image(); img.src = “/test.jpg”; //经过FileReader等 img.onload = function(){ //画到canvas上,位置为x = 10, y = 10 ctx.drawImage(this, 10, 10); } function blackWhite() { var imgData = ctx.getImageData(10, 10, 31, 30); ctx.putImageData(imgData, 50, 10); console.log(imgData, imgData.data); }复制代码
这个的效果是把某张图片原封不动地再画一个,以下图所示:
如今对imgData作一个灰化处理,这个imgData是什么东西呢?它是一个一维数组,存放了从左到右,从上到下每一个像素点的rgba值,以下图所示:
这张图片尺寸为31 * 30,因此数组的大小为31 * 30 * 4 = 3720,而且因为这张图片没有透明通道,因此a的值都为255。
经常使用的灰化算法有如下两种:
(1)平均值
Gray = (Red + Green + Blue) / 3
(2)按人眼对三原色的感知度:绿 > 红 > 蓝
Gray = (Red * 0.3 + Green * 0.59 + Blue * 0.11)
第二种方法更符合客观实际,咱们采用第二种方法,以下代码所示:
function blackWhite() { var imgData = ctx.getImageData(10, 10, 31, 30); var data = imgData.data; var length = data.length; for(var i = 0; i < length; i += 4){ var grey = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; data[i] = data[i + 1] = data[i + 2] = grey; } ctx.putImageData(imgData, 50, 10); }复制代码
执行的效果以下图所示:
其它的滤镜效果,如模糊、锐化、去斑等,读者有兴趣可继续查找资料。
还有一种是图形算法
以下须要计算两个多边形的交点:
这个就涉及到图形算法,能够认为图形算法是对矢量图形的处理,和图像处理是对所有的rgba值处理相对。这个算法也多种多样,其中一个可参考《A new algorithm for computing Boolean operations on polygons》
综合以上,本篇讨论了几个话题:
本篇从前端的角度对一些算法作一些分析和总结,只列了一些我认为比较重要,其它的还有不少没有说起。算法和数据结构是一个永恒的话题,它的目的是用最小的时间和最小的空间解决问题。可是有时候不用太拘泥于必定要最优的答案,可以合适地解决问题就是好方法,并且对于不一样的应用场景可能要采起不一样的策略。反之,若是你的代码里面动不动就是三四重循环,还有嵌套了不少if-else,你可能要考虑下采用合适的数据结构和算法去优化你的代码。