xml地图|网站地图|网站标签 [设为首页] [加入收藏]
深入解读JavaScript面向对象编程实践,实现数据压
分类:web前端

setTimeout 的黑魔法

2016/05/03 · JavaScript · 1 评论 · settimeout

原文出处: 李三思   

setTimeout,前端工程师必定会打交道的一个函数.它看上去非常的简单,朴实.有着一个很不平凡的名字–定时器.让年少的我天真的以为自己可以操纵未来.却不知朴实之中隐含着惊天大密.我还记得我第一次用这个函数的时候,我天真的以为它就是js实现多线程的工具.当时用它实现了一个坦克大战的小游戏,玩儿不亦乐乎.可是随着在前端这条路上越走越远,对它理解开始产生了变化.它似乎开始蒙上了面纱,时常有一些奇怪的表现让我捉摸不透.终于,我的耐心耗尽,下定决心,要撕开它的面具,一探究竟.

要说setTimeout的渊源,就得从它的官方定义说起.w3c是这么定义的

setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。

看到这样一个说明,我们明白了它就是一个定时器,我们设定的函数就是一个”闹钟”,时间到了它就会去执行.然而聪明的你不禁有这样一个疑问,如果是settimeout(fn,0)呢?按照定义的说明,它是否会立马执行?实践是检验真理的唯一标准,让我们来看看下面的实验

JavaScript

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script> alert(1); setTimeout("alert(2)", 0); alert(3); </script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    
    <script>
        alert(1);
        setTimeout("alert(2)", 0);
        alert(3);
    </script>
</body>
</html>

这是一个很简单的实验,如果settimeout(0)会立即执行,那么这里的执行结果就应该是1->2>3  . 然而实际的结果却是1->3->2. 这说明了settimeout(0)并不是立即执行.同时让我们对settimeout的行为感到很诡异.

深入解读JavaScript面向对象编程实践

2016/03/14 · JavaScript · 4 评论 · 面向对象

原文出处: 景庄(@ali景庄)   

面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式,主要包括模块化、多态、和封装几种技术。对JavaScript而言,其核心是支持面向对象的,同时它也提供了强大灵活的基于原型的面向对象编程能力。

本文将会深入的探讨有关使用JavaScript进行面向对象编程的一些核心基础知识,包括对象的创建,继承机制,最后还会简要的介绍如何借助ES6提供的新的类机制重写传统的JavaScript面向对象代码。

利用 canvas 实现数据压缩

2016/03/15 · HTML5 · 1 评论 · Canvas

原文出处: EtherDream   

js引擎是单线程执行的

我们先把上面的问题放一放.从js语言的设计上来看看是否能找到蛛丝马迹.

我们发现js语言设计的一个很重要的点是,js是没有多线程的.js引擎的执行是单线程执行.这个特性曾经困扰我很久,我想不明白既然js是单线程的,那么是谁来为定时器计时的?是谁来发送ajax请求的?我陷入了一个盲区.即将js等同于浏览器.我们习惯了在浏览器里面执行代码,却忽略了浏览器本身.js引擎是单线程的,可是浏览器却可以是多线程的,js引擎只是浏览器的一个线程而已.定时器计时,网络请求,浏览器渲染等等.都是由不同的线程去完成的. 口说无凭,咱们依然看一个例子

JavaScript

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> </body> <script> var isEnd = true; window.setTimeout(function () { isEnd = false;//1s后,改变isEnd的值 }, 1000); while (isEnd); alert('end'); </script> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    
</body>
<script>
    var isEnd = true;
    window.setTimeout(function () {
        isEnd = false;//1s后,改变isEnd的值
    }, 1000);
    while (isEnd);
    alert('end');
</script>
</html>

isEnd默认是true的,在while中是死循环的.最后的alert是不会执行的. 我添加了一个定时器,1秒后将isEnd改为false. 如果说js引擎是多线程的,那么在1秒后,alert就会被执行.然而实际情况是,页面会永远死循环下去.alert并没有执行.这很好的证明了,settimeout并不能作为多线程使用.js引擎执行是单线程的.

面向对象的几个概念

在进入正题前,先了解传统的面向对象编程(例如Java)中常会涉及到的概念,大致可以包括:

  • 类:定义对象的特征。它是对象的属性和方法的模板定义。
  • 对象(或称实例):类的一个实例。
  • 属性:对象的特征,比如颜色、尺寸等。
  • 方法:对象的行为,比如行走、说话等。
  • 构造函数:对象初始化的瞬间被调用的方法。
  • 继承:子类可以继承父类的特征。例如,猫继承了动物的一般特性。
  • 封装:一种把数据和相关的方法绑定在一起使用的方法。
  • 抽象:结合复杂的继承、方法、属性的对象能够模拟现实的模型。
  • 多态:不同的类可以定义相同的方法或属性。

在JavaScript的面向对象编程中大体也包括这些。不过在称呼上可能稍有不同,例如,JavaScript中没有原生的“类”的概念,
而只有对象的概念。因此,随着你认识的深入,我们会混用对象、实例、构造函数等概念。

前言

HTTP 支持 GZip 压缩,可节省不少传输资源。但遗憾的是,只有下载才有,上传并不支持。

如果上传也能压缩,那就完美了。特别适合大量文本提交的场合,比如博客园,就是很好的例子。

虽然标准不支持「上传压缩」,但仍可以自己来实现。

event loop

从上面的实验中,我们更加疑惑了,settimeout到底做了什么事情呢?

原来还是得从js语言的设计上寻找答案.

图片 1

js引擎单线程执行的,它是基于事件驱动的语言.它的执行顺序是遵循一个叫做事件队列的机制.从图中我们可以看出,浏览器有各种各样的线程,比如事件触发器,网络请求,定时器等等.线程的联系都是基于事件的.js引擎处理到与其他线程相关的代码,就会分发给其他线程,他们处理完之后,需要js引擎计算时就是在事件队列里面添加一个任务. 这个过程中,js并不会阻塞代码等待其他线程执行完毕,而且其他线程执行完毕后添加事件任务告诉js引擎执行相关操作.这就是js的异步编程模型.

如此我们再回过头来看settimeout(0)就会恍然大悟.js代码执行到这里时,会开启一个定时器线程,然后继续执行下面的代码.该线程会在指定时间后往事件队列里面插入一个任务.由此可知settimeout(0)里面的操作会放在所有主线程任务之后. 这也就解释了为什么第一个实验结果是1->3-2 .

由此可见官方对于settimeout的定义是有迷惑性的.应该给一个新的定义:

在指定时间内, 将任务放入事件队列,等待js引擎空闲后被执行.

对象(类)的创建

在JavaScript中,我们通常可以使用构造函数来创建特定类型的对象。诸如Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。
此外,我们也可以创建自定义的构造函数。例如:

function Person(name, age, job) { this.name = name; this.age = age; this.job = job; } var person1 = new Person('Weiwei', 27, 'Student'); var person2 = new Person('Lily', 25, 'Doctor');

1
2
3
4
5
6
7
8
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}
 
var person1 = new Person('Weiwei', 27, 'Student');
var person2 = new Person('Lily', 25, 'Doctor');

按照惯例,构造函数始终都应该以一个大写字母开头(和Java中定义的类一样),普通函数则小写字母开头。
要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

  1. 创建一个新对象(实例)
  2. 将构造函数的作用域赋给新对象(也就是重设了this的指向,this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

有关new操作符的更多内容请参考这篇文档。

在上面的例子中,我们创建了Person的两个实例person1person2
这两个对象默认都有一个constructor属性,该属性指向它们的构造函数Person,也就是说:

console.log(person1.constructor == Person); //true console.log(person2.constructor == Person); //true

1
2
console.log(person1.constructor == Person);  //true
console.log(person2.constructor == Person);  //true

Flash

首选方案当然是 Flash,毕竟它提供了压缩 API。除了 zip 格式,还支持 lzma 这种超级压缩。

因为是原生接口,所以性能极高。而且对应的 swf 文件,也非常小。

js引擎与GUI引擎是互斥的

谈到这里,就不得不说浏览器的另外一个引擎—GUI渲染引擎. 在js中渲染操作也是异步的.比如dom操作的代码会在事件队列中生成一个任务,js执行到这个任务时就会去调用GUI引擎渲染.

js语言设定js引擎与GUI引擎是互斥的,也就是说GUI引擎在渲染时会阻塞js引擎计算.原因很简单,如果在GUI渲染的时候,js改变了dom,那么就会造成渲染不同步. 我们需要深刻理解js引擎与GUI引擎的关系,因为这与我们平时开发息息相关,我们时长会遇到一些很奇葩的渲染问题.看这个例子

JavaScript

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <table border=1> <tr><td><button id='do'>Do long calc - bad status!</button></td> <td><div id='status'>Not Calculating yet.</div></td> </tr> <tr><td><button id='do_ok'>Do long calc - good status!</button></td> <td><div id='status_ok'>Not Calculating yet.</div></td> </tr> </table> <script> function long_running(status_div) { var result = 0; for (var i = 0; i < 1000; i++) { for (var j = 0; j < 700; j++) { for (var k = 0; k < 300; k++) { result = result + i + j + k; } } } document.querySelector(status_div).innerHTML = 'calclation done' ; } document.querySelector('#do').onclick = function () { document.querySelector('#status').innerHTML = 'calculating....'; long_running('#status'); }; document.querySelector('#do_ok').onclick = function () { document.querySelector('#status_ok').innerHTML = 'calculating....'; window.setTimeout(function (){ long_running('#status_ok') }, 0); }; </script> </body> </html>

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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <table border=1>
        <tr><td><button id='do'>Do long calc - bad status!</button></td>
            <td><div id='status'>Not Calculating yet.</div></td>
        </tr>
        <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
            <td><div id='status_ok'>Not Calculating yet.</div></td>
        </tr>
    </table>    
<script>
 
function long_running(status_div) {
 
    var result = 0;
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    document.querySelector(status_div).innerHTML = 'calclation done' ;
}
 
document.querySelector('#do').onclick = function () {
    document.querySelector('#status').innerHTML = 'calculating....';
    long_running('#status');
};
 
document.querySelector('#do_ok').onclick = function () {
    document.querySelector('#status_ok').innerHTML = 'calculating....';
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
};
 
</script>
</body>
</html>

我们希望能看到计算的每一个过程,我们在程序开始,计算,结束时,都执行了一个dom操作,插入了代表当前状态的字符串,Not Calculating yet.和calculating….和calclation done.计算中是一个耗时的3重for循环. 在没有使用settimeout的时候,执行结果是由Not Calculating yet 直接跳到了calclation done.这显然不是我们希望的.而造成这样结果的原因正是js的事件循环单线程机制.dom操作是异步的,for循环计算是同步的.异步操作都会被延迟到同步计算之后执行.也就是代码的执行顺序变了.calculating….和calclation done的dom操作都被放到事件队列后面而且紧跟在一起,造成了丢帧.无法实时的反应.这个例子也告诉了我们,在需要实时反馈的操作,如渲染等,和其他相关同步的代码,要么一起同步,要么一起异步才能保证代码的执行顺序.在js中,就只能让同步代码也异步.即给for计算加上settimeout.

自定义对象的类型检测

我们可以使用instanceof操作符进行类型检测。我们创建的所有对象既是Object的实例,同时也是Person的实例。
因为所有的对象都继承自Object

console.log(person1 instanceof Object); //true console.log(person1 instanceof Person); //true console.log(person2 instanceof Object); //true console.log(person2 instanceof Person); //true

1
2
3
4
console.log(person1 instanceof Object);  //true
console.log(person1 instanceof Person);  //true
console.log(person2 instanceof Object);  //true
console.log(person2 instanceof Person);  //true

JavaScript

Flash 逐渐淘汰,但取而代之的 HTML5,却没有提供压缩 API。只能自己用 JS 实现。

这虽然可行,但运行速度就慢多了,而且相应的 JS 也很大。

如果代码有 50kb,而数据压缩后只小 10kb,那就不值了。除非量大,才有意义。

settimeout(0)的作用

不同浏览器的实现情况不同,HTML5定义的最小时间间隔是4毫秒. 使用settimeout(0)会使用浏览器支持的最小时间间隔.所以当我们需要把一些操作放到下一帧处理的时候,我们通常使用settimeout(0)来hack.

构造函数的问题

我们不建议在构造函数中直接定义方法,如果这样做的话,每个方法都要在每个实例上重新创建一遍,这将非常损耗性能。
——不要忘了,ECMAScript中的函数是对象,每定义一个函数,也就实例化了一个对象。

幸运的是,在ECMAScript中,我们可以借助原型对象来解决这个问题。

其他

能否不用 JS,而是利用某些接口,间接实现压缩?

事实上,在 HTML5 刚出现时,就注意到了一个功能:canvas 导出图片。可以生成 jpg、png 等格式。

如果在思考的话,相信你也想到了。没错,就是 png —— 它是无损压缩的。

我们把普通数据当成像素点,画到 canvas 上,然后导出成 png,就是一个特殊的压缩包了~


下面开始探索。。。

requestAnimationFrame

这个函数与settimeout很相似,但它是专门为动画而生的.settimeout经常被用来做动画.我们知道动画达到60帧,用户就无法感知画面间隔.每一帧大约16毫秒.而requestAnimationFrame的帧率刚好是这个频率.除此之外相比于settimeout,还有以下的一些优点:

  • requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧,每帧大约16毫秒.
  • 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
  • 但它优于setTimeout/setInterval的地方在于它是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。

借助原型模式定义对象的方法

我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向该函数的原型对象
该对象包含了由特定类型的所有实例共享的属性和方法。也就是说,我们可以利用原型对象来让所有对象实例共享它所包含的属性和方法。

function Person(name, age, job) { this.name = name; this.age = age; this.job = job; } // 通过原型模式来添加所有实例共享的方法 // sayName() 方法将会被Person的所有实例共享,而避免了重复创建 Person.prototype.sayName = function () { console.log(this.name); }; var person1 = new Person('Weiwei', 27, 'Student'); var person2 = new Person('Lily', 25, 'Doctor'); console.log(person1.sayName === person2.sayName); // true person1.sayName(); // Weiwei person2.sayName(); // Lily

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}
 
// 通过原型模式来添加所有实例共享的方法
// sayName() 方法将会被Person的所有实例共享,而避免了重复创建
Person.prototype.sayName = function () {
  console.log(this.name);
};
 
var person1 = new Person('Weiwei', 27, 'Student');
var person2 = new Person('Lily', 25, 'Doctor');
 
console.log(person1.sayName === person2.sayName); // true
 
person1.sayName(); // Weiwei
person2.sayName(); // Lily

正如上面的代码所示,通过原型模式定义的方法sayName()为所有的实例所共享。也就是,
person1person2访问的是同一个sayName()函数。同样的,公共属性也可以使用原型模式进行定义。例如:

function Chinese (name) { this.name = name; } Chinese.prototype.country = 'China'; // 公共属性,所有实例共享

1
2
3
4
5
function Chinese (name) {
    this.name = name;
}
 
Chinese.prototype.country = 'China'; // 公共属性,所有实例共享

数据转换

数据转像素,并不麻烦。1 个像素可以容纳 4 个字节:

R = bytes[0] G = bytes[1] B = bytes[2] A = bytes[3]

1
2
3
4
R = bytes[0]
G = bytes[1]
B = bytes[2]
A = bytes[3]

事实上有现成的方法,可批量将数据填充成像素:

img = new ImageData(bytes, w, h); context.putImageData(img, w, h)

1
2
img = new ImageData(bytes, w, h);
context.putImageData(img, w, h)

但是,图片的宽高如何设定?

总结:

  1. 浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。
  2. javascript引擎是基于事件驱动单线程执行的.JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。
  3. 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  4. 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

    2 赞 11 收藏 1 评论

图片 2

原型对象

现在我们来深入的理解一下什么是原型对象。

只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。
也就是说:Person.prototype.constructor指向Person构造函数。

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。
当调用构造函数创建一个新实例后,该实例内部将包含一个指针(内部属性),指向构造函数的原型对象。ES5中称这个指针为[[Prototype]]
在Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__(目前已被废弃);而在其他实现中,这个属性对脚本则是完全不可见的。
要注意,这个链接存在于实例与构造函数的原型对象之间,而不是实例与构造函数之间

这三者关系的示意图如下:

图片 3

上图展示了Person构造函数、Person的原型对象以及Person现有的两个实例之间的关系。

  • Person.prototype指向了原型对象
  • Person.prototype.constructor又指回了Person构造函数
  • Person的每个实例person1person2都包含一个内部属性(通常为__proto__),person1.__proto__person2.__proto__指向了原型对象

尺寸设定

最简单的,就是用 1px 的高度。比如有 1000 个像素,则填在 1000 x 1 的图片里。

但如果有 10000 像素,就不可行了。因为 canvas 的尺寸,是有限制的。

不同的浏览器,最大尺寸不一样。有 4096 的,也有 32767 的。。。

以最大 4096 为例,如果每次都用这个宽度,显然不合理。

比如有 n = 4100 个像素,我们使用 4096 x 2 的尺寸:

| 1 | 2 | 3 | 4 | ... | 4095 | 4096 | | 4097 | 4098 | 4099 | 4100 | ...... 未利用 ......

1
2
| 1    | 2    | 3    | 4    | ...  | 4095 | 4096 |
| 4097 | 4098 | 4099 | 4100 | ...... 未利用 ......

第二行只用到 4 个,剩下的 4092 个都空着了。

但 4100 = 41 * 100。如果用这个尺寸,就不会有浪费。

所以,得对 n 分解因数:

n = w * h

1
n = w * h

这样就能将 n 个像素,正好填满 w x h 的图片。

但 n 是质数的话,就无解了。这时浪费就不可避免了,只是,怎样才能浪费最少?

于是就变成这样一个问题:

如何用 n + m 个点,拼成一个 w x h 的矩形(0

考虑到 MAX 不大,穷举就可以。

我们遍历 h,计算相应的 w = ceil(n / h), 然后找出最接近 n 的 w * h。

var beg = Math.ceil(n / MAX); var end = Math.ceil(Math.sqrt(n)); var minSize = 9e9; var bestH = 0, // 最终结果 bestW = 0; for (h = beg; h end; h++) { var w = Math.ceil(n / h); var size = w * h; if (size minSize) { minSize = size; bestW = w; bestH = h; } if (size == n) { break; } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var beg = Math.ceil(n / MAX);
var end = Math.ceil(Math.sqrt(n));
 
var minSize = 9e9;
 
var bestH = 0,          // 最终结果
    bestW = 0;
 
for (h = beg; h  end; h++) {
    var w = Math.ceil(n / h);
    var size = w * h;
 
    if (size  minSize) {
        minSize = size;
        bestW = w;
        bestH = h;
    }
    if (size == n) {
        break;
    }
}

因为 w * h 和 h * w 是一样的,所以只需遍历到 sqrt(n) 就可以。

同样,也无需从 1 开始,从 n / MAX 即可。

这样,我们就能找到最适合的图片尺寸。

当然,连续的空白像素,最终压缩后会很小。这一步其实并不特别重要。

查找对象属性

从上图我们发现,虽然Person的两个实例都不包含属性和方法,但我们却可以调用person1.sayName()
这是通过查找对象属性的过程来实现的。

  1. 搜索首先从对象实例本身开始(实例person1sayName属性吗?——没有)
  2. 如果没找到,则继续搜索指针指向的原型对象person1.__proto__sayName属性吗?——有)

这也是多个对象实例共享原型所保存的属性和方法的基本原理。

注意,如果我们在对象的实例中重写了某个原型中已存在的属性,则该实例属性会屏蔽原型中的那个属性。
此时,可以使用delete操作符删除实例上的属性。

渲染问题

定下尺寸,我们就可以「渲染数据」了。

然而现实中,总有些意想不到的坑。canvas 也不例外:

<canvas id="canvas" width="100" heigth="100"></canvas> <script> var ctx = canvas.getContext('2d'); // 写入的数据 var bytes = [100, 101, 102, 103]; var buf = new Uint8ClampedArray(bytes); var img = new ImageData(buf, 1, 1); ctx.putImageData(img, 0, 0); // 读取的数据 img = ctx.getImageData(0, 0, 1, 1); console.log(img.data); // chrome [99, 102, 102, 103] // firefox [101, 101, 103, 103] // ... </script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<canvas id="canvas" width="100" heigth="100"></canvas>
<script>
  var ctx = canvas.getContext('2d');
 
  // 写入的数据
  var bytes = [100, 101, 102, 103];
 
  var buf = new Uint8ClampedArray(bytes);
  var img = new ImageData(buf, 1, 1);
  ctx.putImageData(img, 0, 0);
 
  // 读取的数据
  img = ctx.getImageData(0, 0, 1, 1);
  console.log(img.data);
  // chrome  [99,  102, 102, 103]
  // firefox [101, 101, 103, 103]
  // ...
</script>

读取的像素,居然和写入的有偏差!而且不同的浏览器,偏差还不一样。

原来,浏览器为了提高渲染性能,有一个 Premultiplied Alpha 的机制。但是,这会牺牲一些精度!

虽然视觉上并不明显,但用于数据存储,就有问题了。

如何禁用它?一番尝试都没成功。于是,只能从数据上琢磨了。

如果不使用 Alpha 通道,又会怎样?

// 写入的数据 var bytes = [100, 101, 102, 255]; ... console.log(img.data); // [100, 101, 102, 255]

1
2
3
4
  // 写入的数据
  var bytes = [100, 101, 102, 255];
  ...
  console.log(img.data);  // [100, 101, 102, 255]

这样,倒是避开了问题。

看来,只能从数据上着手,跳过 Alpha 通道:

// pixel 1 new_bytes[0] = bytes[0] // R new_bytes[1] = bytes[1] // G new_bytes[2] = bytes[2] // B new_bytes[3] = 255 // A // pixel 2 new_bytes[4] = bytes[3] // R new_bytes[5] = bytes[4] // G new_bytes[6] = bytes[5] // B new_bytes[7] = 255 // A ...

1
2
3
4
5
6
7
8
9
10
11
12
13
// pixel 1
new_bytes[0] = bytes[0]     // R
new_bytes[1] = bytes[1]     // G
new_bytes[2] = bytes[2]     // B
new_bytes[3] = 255          // A
 
// pixel 2
new_bytes[4] = bytes[3]     // R
new_bytes[5] = bytes[4]     // G
new_bytes[6] = bytes[5]     // B
new_bytes[7] = 255          // A
 
...

这时,就不受 Premultiplied Alpha 的影响了。

出于简单,也可以 1 像素存 1 字节:

// pixel 1 new_bytes[0] = bytes[0] new_bytes[1] = 255 new_bytes[2] = 255 new_bytes[3] = 255 // pixel 2 new_bytes[4] = bytes[1] new_bytes[5] = 255 new_bytes[6] = 255 new_bytes[7] = 255 ...

1
2
3
4
5
6
7
8
9
10
11
12
13
// pixel 1
new_bytes[0] = bytes[0]
new_bytes[1] = 255
new_bytes[2] = 255
new_bytes[3] = 255
 
// pixel 2
new_bytes[4] = bytes[1]
new_bytes[5] = 255
new_bytes[6] = 255
new_bytes[7] = 255
 
...

这样,整个图片最多只有 256 色。如果能导出成「索引型 PNG」的话,也是可以尝试的。

Object.getPrototypeOf()

根据ECMAScript标准,someObject.[[Prototype]] 符号是用于指派 someObject 的原型。
这个等同于 JavaScript 的 __proto__ 属性(现已弃用)。
从ECMAScript 5开始, [[Prototype]] 可以用Object.getPrototypeOf()Object.setPrototypeOf()访问器来访问。

其中Object.getPrototypeOf()在所有支持的实现中,这个方法返回[[Prototype]]的值。例如:

person1.__proto__ === Object.getPrototypeOf(person1); // true Object.getPrototypeOf(person1) === Person.prototype; // true

1
2
person1.__proto__ === Object.getPrototypeOf(person1); // true
Object.getPrototypeOf(person1) === Person.prototype; // true

也就是说,Object.getPrototypeOf(p1)返回的对象实际就是这个对象的原型。
这个方法的兼容性请参考该链接)。

数据编码

最后,就是将图像进行导出。

如果 canvas 能直接导出成 blob,那是最好的。因为 blob 可通过 AJAX 上传。

canvas.toBlob(function(blob) { // ... }, 'image/png')

1
2
3
canvas.toBlob(function(blob) {
    // ...
}, 'image/png')

不过,大多浏览器都不支持。只能导出 data uri 格式:

uri = canvas.toDataURL('image/png') // data:image/png;base64,xxxx

1
uri = canvas.toDataURL('image/png')  // data:image/png;base64,xxxx

但 base64 会增加长度。所以,还得解回二进制:

base64 = uri.substr(uri.indexOf(',') + 1) binary = atob(base64)

1
2
base64 = uri.substr(uri.indexOf(',') + 1)
binary = atob(base64)

这时的 binary,就是最终数据了吗?

如果将 binary 通过 AJAX 提交的话,会发现实际传输字节,比 binary.length 大。

原来 atob 返回的数据,仍是字符串型的。传输时,就涉及字集编码了。

因此还需再转换一次,变成真正的二进制数据:

var len = binary.length var buf = new Uint8Array(len) for (var i = 0; i len; i++) { buf[i] = binary.charCodeAt(i) }

1
2
3
4
5
6
var len = binary.length
var buf = new Uint8Array(len)
 
for (var i = 0; i  len; i++) {
    buf[i] = binary.charCodeAt(i)
}

这时的 buf,才能被 AJAX 原封不动的传输。

Object.keys()

要取得对象上所有可枚举的实例属性,可以使用ES5中的Object.keys()方法。例如:

Object.keys(p1); // ["name", "age", "job"]

1
Object.keys(p1); // ["name", "age", "job"]

此外,如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyName()方法。

最终效果

综上所述,我们简单演示下:Demo

找一个大块的文本测试。例如 qq.com 首页 HTML,有 637,101 字节。

先使用「每像素 1 字节」的编码,各个浏览器生成的 PNG 大小:

Chrome FireFox Safari
体积 289,460 203,276 478,994
比率 45.4% 31.9% 75.2%

其中火狐压缩率最高,减少了 2/3 的体积。

生成的 PNG 看起来是这样的:

图片 4

不过遗憾的是,所有浏览器生成的图片,都不是「256 色索引」的。


再测试「每像素 3 字节」,看看会不会有改善:

Chrome FireFox Safari
体积 297,239 202,785 384,183
比率 46.7% 31.8% 60.3%

Safari 有了不少的进步,不过 Chrome 却更糟了。

FireFox 有略微的提升,压缩率仍是最高的。

图片 5

同样遗憾的是,即使整个图片并没有用到 Alpha 通道,但生成的 PNG 仍是 32 位的。

而且,也无法设置压缩等级,使得这种压缩方式,效率并不高。

相比 Flash 压缩,差距就大多了:

deflate 压缩 lzma 压缩
体积 133,660 108,015
比率 21.0% 17.0%

并且 Flash 生成的是通用格式,后端解码时,使用标准库即可。

而 PNG 还得位图解码、像素处理等步骤,很麻烦。

所以,现实中还是优先使用 Flash,本文只是开脑洞而已。

本文由澳门新葡亰手机版发布于web前端,转载请注明出处:深入解读JavaScript面向对象编程实践,实现数据压

上一篇:什么是虚拟视窗,Components构建单页面应用 下一篇:也谈JavaScript数组去重,拥抱Web设计新趋势
猜你喜欢
热门排行
精彩图文