wengjq / blog Goto Github PK
View Code? Open in Web Editor NEW个人博客
个人博客
之前看到 vue 文档出现这个单向数据流,就觉得有点纳闷,我擦,vue不是双向绑定的么,出现这个是什么鬼,看了文档里也说了两种情况修改 props 的处理方案。还是有点疑惑。所以有了这篇文章。
从v-model开始讲起
v-model在使用的时候很像双向绑定的(实际上也是。。。),但是 Vue 是单项数据流,v-model 只是语法糖而已:
<input v-model="something" />
<input v-bind:value="something" v-on:input="something = $event.target.value" />
第一行的代码其实只是第二行的语法糖。然后第二行代码还能简写成这样:
<input :value="something" @input="something = $event.target.value" />
要理解这行代码,首先你要知道 input 元素本身有个 oninput 事件,这是 HTML5 新增加的,类似 onchange ,每当输入框内容发生变化,就会触发 oninput ,通过 $event 把最新的 value 传递给 something。
我们仔细观察语法糖和原始语法那两行代码,可以得出一个结论:
在给 input 元素添加 v-model 属性时,默认会把 value 作为元素的属性,然后把 'input' 事件作为实时传递 value 的触发事件
v-model 不仅仅能在 input 上用,在组件上也能使用,拿官网上的demo看。
<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)"\
>\
</span>\
',
props: ['value'], // 为什么这里要用 value 属性,value在哪里定义的?
methods: {
// 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
updateValue: function (value) {
var formattedValue = value
// 删除两侧的空格符
.trim()
// 保留 2 位小数
.slice(
0,
value.indexOf('.') === -1
? value.length
: value.indexOf('.') + 3
)
// 如果值尚不合规,则手动覆盖为合规的值
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// 通过 input 事件带出数值
// <!--为什么这里把 'input' 作为触发事件的事件名?`input` 在哪定义的?-->
this.$emit('input', Number(formattedValue))
}
}
})
如果你知道这两个问题的答案,那么恭喜你真正掌握了 v-model,如果你没明白,那么可以看下这段代码:
<currency-input v-model="price"></currency-input>
所以在组件中使用时,它相当于下面的简写:
//上行代码是下行的语法糖
<currency-input :value="price" @input="price = arguments[0]"></currency-input>
所以,给组件添加 v-model 属性时,默认会把 value 作为组件的属性,然后把 'input' 值作为给组件绑定事件时的事件名。这在写组件时特别有用。
在创建类似复选框或者单选框的常见组件时,v-model就不好用了。
<input type="checkbox" v-model="something" />
v-model 给我们提供好了 value 属性和 oninput 事件,但是,我们需要的不是 value 属性,而是 checked 属性,并且当你点击这个单选框的时候不会触发 oninput 事件,它只会触发 onchange 事件。
因为 v-model 只是用到了 input 元素上,所以这种情况好解决:
<input type="checkbox" :checked="value" @change="change(value, $event)"
当 v-model 用到组件上时:
<checkbox v-model="value"></checkbox>
Vue.component('checkbox', {
tempalte: '<input type="checkbox" @change="change" :checked="currentValue"/>'
props: ['value'],
data: function () {
return {
//这里为什么要定义一个局部变量,并用 prop 的值初始化它。
currentValue: this.value
};
},
methods: {
change: function ($event) {
this.currentValue = $event.target.checked;
this.$emit('input', this.currentValue);
}
})
在 Vue 2.2 版本,你可以在定义组件时通过 model 选项的方式来定制 prop/event 。
从上面 v-model 的分析我们可以这么理解,双向数据绑定就是在单向绑定的基础上给可输入元素(input、textare等)添加了 change(input) 事件,来动态修改 model 和 view ,即通过触发($emit)父组件的事件来修改mv来达到 mvvm 的效果。而 vue 组件间传递数据是单向的,即数据总是由父组件传递到子组件,子组件在其内部可以有自己维护的数据,但它无权修改父组件传递给它的数据,当开发者尝试这样做的时候,vue 将会报错。这样做是为了组件间更好的解耦,在开发中可能有多个子组件依赖于父组件的某个数据,假如子组件可以修改父组件数据的话,一个子组件变化会引发所有依赖这个数据的子组件发生变化,所以 vue 不推荐子组件修改父组件的数据,直接修改 props 会抛出警告。流程图如下:
所以,当你想要在子组件去修改 props 时,把这个子组件当成父组件那样用,所以就有了
开发产品功能的时候ui往往会给出虚线边框的效果图,于是乎,我们往往第一时间想到的是用css里的border,可是border里一般就提供两种效果,dashed或者dotted,ui这时就不满意了,说虚线太密了。废话不多说,下面直接给解决方案(参考css揭秘):
div {
padding: 1em;
border: 1px dashed transparent;
background: linear-gradient(white,white) padding-box,
repeating-linear-gradient(-45deg,#ccc 0, #ccc 0.25em,white 0,white 0.75em);
}
基本效果如下:
以上的基本原理是通过两层线性渐变背景去覆盖,第一层是在padding-box容器内(及虚线边框的容器内的白色部分,如果换成border-box那肯定把虚线也覆盖了),用这一层去覆盖repeating-linear-gradient生成的条纹背景。具体的虚线的颜色和间距都可以通过repeating-linear-gradient生成的条纹背景去调整。最后给出 linear-gradient 支持的浏览器,要使用的话请权衡。
观察者模式( 又叫发布-订阅者模式 )应该是最常用的模式之一,在很多语言里都得到大量应用。包括我们平时接触的 dom 事件,也是 js 和 dom 之间实现的一种观察者模式。
document.body.addEventListener("click", function() {
alert("Hello World");
});
document.body.click();
在这里需要监控用户点击 document.body 的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅了 document.body 的 click 事件,当 body 节点被点击时,body 节点便会向订阅者发布这个消息。那么到底什么是观察者模式呢?
先看看生活中的观察者模式。例如求职者去公司面试的时候,面试后每个面试官都会对求职者说:“有消息我们会通知你”。 在这里“我”是订阅者, 面试官是发布者。所以我不用每天或者每小时都去询问面试结果, 通讯的主动权掌握在了面试官手上。而我只需要提供一个联系方式。
观察者模式定义了对象之间的一对多依赖关系,用于当一个对象改变状态时,所有的依赖关系都会被自动通知和更新。
观察者模式提供了一个订阅模型,其中对象订阅事件并在事件发生时得到通知。 这种模式是事件驱动的编程的基石,包括 JavaScript 。 观察者模式有利于良好的面向对象的设计,并促进松耦合。
示例图如下:
当一个对象的状态或动作取决于另一个对象的状态或动作时。
当更改一个对象时,需要更改未知数量的其他对象。
当一个对象应该能够通知其他对象的变化而不知道这些其他对象。
var pubsub = {};
(function(q) {
var topics = {},
subUid = -1;
// 使用特定主题名称和参数(如要传递的数据)发布事件
q.publish = function(topic, args) {
if (!topics[topic]) {
return false;
}
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func( topic, args );
}
return this;
};
// 当观察到话题/事件时,用特定的话题名称和回调函数来订阅事件
q.subscribe = function(topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = ( ++subUid ).toString();
topics[topic].push({
token: token,
func: func
});
return token;
};
// 根据对订阅的标记化引用,取消订阅特定主题
q.unsubscribe = function(token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice( i, 1 );
return token;
}
}
}
}
return this;
};
}(pubsub));
var messageLogger = function (topics, data) {
console.log( "Logging: " + topics + ": " + data );
};
// 订阅
var subscription = pubsub.subscribe("inbox/newMessage", messageLogger);
// 发布
pubsub.publish("inbox/newMessage", "hello world!");
// 退订
//pubsub.unsubscribe(subscription);
// 发布
pubsub.publish("inbox/newMessage", "Hello! are you still there?");
前端页面倒计时功能在很多场景中会用到,如 mobi 手机端的欢迎页倒计时、商城功能的秒杀活动等,这些功能往往对时间的精确更高,下面会分享下常见的坑点及如何解决。
用现有 mobi 手机端欢迎页倒计时为例,以下是功能截图。
代码如下:
var second = 10; // 倒计时时间为 10 s
var timer;
var timer_div = $('#timer_div');
var start = new Date().getTime();
var count = 0;
clearInterval(timer);
timer = setInterval(showTime, 1000);
function showTime() {
if (second === 0) {
...
clearInterval(timer);
return false;
}
count++;
console.log(new Date().getTime() - (start + count * 1000)); // 这里代码运行结果,定时器每秒执行一次,每次输出应该是0 。
timer_div.html('<div>' + second + 's</div>');
second--;
}
结论:由于代码执行占用时间和其他事件阻塞原因,导致有些事件执行延迟了几ms,但影响还不是很大。
下面加一段阻塞线程的代码看看:
var start = new Date().getTime();
var count = 0;
// 占用线程事件
setInterval(function () {
var j = 0;
while(j++ < 100000000);
}, 0);
//定时器测试
setInterval(function () {
count++;
console.log(new Date().getTime() - (start + count * 1000));
}, 1000);
以上代码实际输出如下:
结论:由于加了很占线程的阻塞事件,导致定时器事件每次执行延迟越来越严重。
以上的阻塞线程的代码还不算很极端,假如在执行定时器的过程中有同步 ui 事件的代码,同步代码会立即执行。实际上在移动端的滚动页面中是有可能出现这种情况的,以下是一个例子。
function runForSeconds(s) {
var start = +new Date();
while (start + s * 1000 > (+new Date())) {}
}
document.body.addEventListener("click", function () {
runForSeconds(10);
}, false);
setTimeout(function () {
console.log("Done!");
}, 1000 * 3);
时间线对比:
等待 3 秒 |----1s----|----2s----|----3s----|--->console.log("Done!");
经过 2 秒 |----1s----|----2s----| ----------|-->console.log("Done!");
点击 body 后
以为是这样:|----1s----|----2s----|----3s----|--->console.log("Done!")--->|------------------10s----------------|
其实是这样:|----1s----|----2s----|------------------10s----------------|--->console.log("Done!");
结论:如果有同步的 ui 事件代码出现,实际功能的倒计时基本“失效”了,这是不同浏览器打开相同的倒计时页面往往误差非常大。
分析一下从获取服务器时间到前端显示倒计时的过程:
客户端 http 请求服务器时间;
服务器响应完成;
服务器通过网络传输时间数据到客户端;
客户端根据活动开始时间和服务器时间差做倒计时显示;
服务器响应完成的时间其实就是服务器时间,但经过网络传输这一步,就会产生误差了,误差大小视网络环境而异,这部分时间前端也没有什么好办法计算出来,一般是几十 ms 以内,大的可能有几百 ms 。
可以得出:当前服务器时间 = 服务器系统返回时间 + 网络传输时间 + 前端渲染时间 + 常量(可选),这里重点是说要考虑前端渲染的时间,避免不同浏览器渲染快慢差异造成明显的时间不同步,这是第一点。(网络传输时间忽略或加个常量),前端渲染时间可以在服务器返回当前时间和本地前端的时间的差值得出。
获得服务器时间后,前端进入倒计时计算和计时器显示,这步就要考虑 js 代码冻结和线程阻塞造成计时器延时问题了,思路是通过引入计数器,判断计时器延迟执行的时间来调整,尽量让误差缩小,不同浏览器不同时间段打开页面倒计时误差可控制在 1s 以内。
// 继续线程占用
setInterval(function () {
var j = 0;
while(j++ < 100000000);
}, 0);
//倒计时
var interval = 1000,
ms = 50000, // 从服务器和活动开始时间计算出的时间差,这里测试用 50000ms
count = 0,
startTime = new Date().getTime();
if (ms >= 0) {
var timeCounter = setTimeout(countDownStart, interval);
}
function countDownStart() {
count++;
var offset = new Date().getTime() - (startTime + count * interval);
var nextTime = interval - offset;
var daytohour = 0;
if (nextTime < 0) {
nextTime = 0
};
ms -= interval;
console.log("误差:" + offset + "ms,下一次执行:" + nextTime + "ms后,离活动开始还有:" + ms + "ms");
if (ms < 0) {
clearTimeout(timeCounter);
} else {
timeCounter = setTimeout(countDownStart, nextTime);
}
}
运行结果如下:
结论:由于线程阻塞延迟问题,做了 setTimeout 执行时间的误差修正,保证 setTimeout 执行时间一致。若冻结时间特别长的,还要做特殊处理。
具有限界条件的 DFS (Depth first search,深度优先搜索)算法称为回溯算法。
LeetCode 的第 22 题
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
For example, given n = 3, a solution set is:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
题目目的是给你一个数 n 写出来所有中可能的括号集合。
本题采用的回溯的解题**:所谓(回溯)Backtracking 都是这样的思路:在当前局面下,你有若干种选择。那么尝试每一种选择。如果已经发现某种选择肯定不行(因为违反了某些限定条件),就返回;如果某种选择试到最后发现是正确解,就将其加入解集。
对于这道题,有以下的限制条件:
1、如果左括号已经用完了,则不能再加左括号
2、如果已经出现的右括号和左括号一样多,则不能再加右括号了。因为那样的话新加入的右括号一定无法匹配。
结束条件是:
左右括号都已经用完。
因此,把上面的思路写一下伪代码:
if (左右括号都已用完) {
加入解集,返回
}
// 否则开始情况
if (还有左括号可以用) {
加一个左括号,继续递归
}
if (右括号小于左括号) {
加一个右括号,继续递归
}
具体实现:
/**
* @param {number} n
* @return {string[]}
*/
var generateParenthesis = function (n) {
var ans = [];
dfs(ans, '', 0, 0, n);
return ans;
};
function dfs(ans, str, left, right, n) {
if (left === n && right === n) ans.push(str);
if (left < n) {
dfs(ans, str + '(', left + 1, right, n);
}
if (right < left) {
dfs(ans, str + ')', left, right + 1, n);
}
}
console.log(generateParenthesis(3)); // ["((()))", "(()())", "(())()", "()(())", "()()()"]
AppCache 已被废弃.考虑使用 service worker 的 Cache API。
如果您使用 console.time() 测量页面的性能,请考虑改用 User Timing API。 其优势包括:
如果使用 Date.now() 测量时间,请考虑改用 performance.now()。performance.now() 可提供较高的时间戳分辨率,并始终以恒定的速率增加,它不受系统时钟(可以调整或手动倾斜)的影响。
对于网速较慢(如 2G、3G 或较慢的 WLAN)的用户,外部脚本通过 document.write() 动态注入会使页面内容的显示延迟数十秒。
以下突变事件会损害性能,在 DOM 事件规范中已弃用。
建议将每个 mutation events 替换成 MutationObserver
2009 年的旧 Flexbox 规范已弃用,其速度比最新的规范慢 2.3 倍。 请参阅 Flexbox 布局并不慢了解更多信息。
页面在加载时自动请求用户位置会使用户不信任页面或感到困惑。应将此请求与用户的手势(如点按一个“Find Stores Near Me”按钮)进行关联,而不是在页面加载时自动请求用户的位置。
如怎样才算好的通知中所述,好的通知需要做到及时、相关且精确。 如果您的页面在加载时要求权限以发送通知,则这些通知可能与您的用户无关或者不是他们的精准需求。为提高用户体验,最好是向用户发送特定类型的通知,并在他们选择加入后显示权限请求。
如果渲染的图像与其源文件中的宽高比不同,则呈现的图像可能看起来失真,产生不好的用户体验。
建议:
入侵者具有自动的 Web 爬虫程序,可以对您的站点进行扫描以查找已知的安全漏洞。 Web 搜寻器检测到漏洞时,会向入侵者发出警报。从那里,入侵者只需要弄清楚如何利用站点上的漏洞。
建议:
停止使用 Lighthouse 标志的每个库。如果该库发布了解决该漏洞的较新版本,请升级到该版本,或考虑使用其他库。
请参阅 Snyk 的漏洞数据库,以了解有关每个库的漏洞的更多信息。
当您的页面链接至使用 target="_blank" 的另一个页面时,新页面将与您的页面在同一个进程上运行。如果新页面正在执行开销极大的 JavaScript,您的页面性能可能会受影响。
此外,target="_blank" 也是一个安全漏洞。新的页面可以通过 window.opener 访问您的窗口对象,并且它可以使用 window.opener.location = newURL 将您的页面导航至不同的网址。
如需了解详细信息,请参阅 rel=noopener 的性能优势。
建议:
将 rel="noopener" 添加至 Lighthouse 在您的报告中识别的每个链接。 一般情况下,当您在新窗口或标签中打开一个外部链接时,始终添加 rel="noopener"。
<a href="https://examplepetstore.com" target="_blank" rel="noopener">...</a>
一些网站声称阻止用户粘贴密码会以某种方式提高安全性。国家网络安全中心在“让他们粘贴密码”中说,这一说法是没有根据的。
密码粘贴可提高安全性,因为它使用户能够使用密码管理器。密码管理器通常会为用户生成强密码,将其安全存储,然后在用户需要登录时自动将其粘贴到密码字段中。
建议:
删除阻止用户粘贴到密码字段中的代码。这可能是在与密码输入元素关联的粘贴事件侦听器中对 preventDefault()的调用。
let input = document.querySelector('input');
input.addEventListener('paste', (e) => {
e.preventDefault(); // This is what prevents pasting.
});
所有网站均应使用 HTTPS 进行保护,即使是不处理敏感数据的网站也应如此。 HTTPS 可防止入侵者篡改或被动地侦听您的网站和您的用户之间的通信。HTTPS 也是许多强大的新网络平台功能(如拍照或录制音频)的前提条件。
根据定义,一个应用如果不在 HTTPS 上运行,那么它就不符合成为 Progressive Web App 的条件。 这是因为许多核心的 Progressive Web App 技术(如服务工作线程)都需要使用 HTTPS。
如果您运行自己的服务器并且需要一个成本低廉且简单的方式来生成证书,请访问 Let's Encrypt。 有关在您的服务器上启用 HTTPS 的更多帮助,请参阅以下文档集:对传输中的数据进行加密。
HTTP/2 可更快地提供页面的资源,并且可减少通过网络传输的数据。
有关 HTTP/2 通过 HTTP/1.1 提供的优势的列表,请参阅 HTTP/2 常见问题解答。
有关深入的技术概览,请参阅 HTTP/2 简介。
在 URLs 下,Lighthouse 列出不是通过 HTTP/2 提供的每个资源。要通过此审查,需要通过 HTTP/2 提供其中的每个资源。
要了解如何在您的服务器上启用 HTTP/2,请参阅设置 HTTP/2。
被动事件是新兴的 Web 标准,可以显著提高滚动性能,尤其在移动设备上。当使用 touch 事件监听器(scroll 事件不存在这个问题)进行滚动时,因为浏览器不知道你是否会取消滚动,它们总是等待监听器执行完毕后才开始滚动,这样就造成了明显的延迟。事件监听器 options 中使用 passive:true 表明监听器永远不会取消滚动,这样浏览器就可以立即滚动。
在支持被动事件侦听器的浏览器中,将侦听器标记为 passive 即可:
建议:
将 passive 标志添加到 Lighthouse 已识别的所有事件侦听器。 一般情况下,将 passive 标志添加到每个没有调用 preventDefault() 的 wheel、mousewheel、touchstart 和 touchmove 事件侦听器。
在支持被动事件侦听器的浏览器中,将侦听器标记为 passive 与设置标志一样简单:
var supportsPassive = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassive = true;
}
});
window.addEventListener("testPassive", null, opts);
window.removeEventListener("testPassive", null, opts);
} catch (e) {}
document.addEventListener('touchstart', onTouchStart, {passive: true});
无论是开发复杂的业务还是写有很多兼容情况的代码都免不了复杂的 if else if 代码,但是有时 if else if 写的多有可能就变成一堆乱麻,可读性越来越差,有没有更好的方式来组织代码呢?下面已一个 js 判断类型的例子来演示 hook 到底如何使用。
// 当然这里只是示例,为什么不转成小写,因为其他场景不是那么刚刚好。。。
function type (obj) {
var t = Object.prototype.toString.call(obj).slice(8, -1);
if (t === "Array") {
return "array"
} else if (t === "Object") {
return "object"
} else if (t === "String") {
return "string"
} else if (t === "Date") {
return "date"
} else if (t === "RegExp") {
return "regexp"
} else if (t === "Function") {
return "function"
} else if (t === "Boolean") {
return "boolean"
} else if (t === "Number") {
return "number"
} else if (t === "Null") {
return "null"
} else if (t === "Undefined") {
return "undefined"
}
}
上面的流程如下图:
可以看到,在其中判断类型的代码中,运用了很多 else if,如果现在类型发生变化,又多了其他一些类型,那么 else if 势必越来越复杂,往后维护代码也将越来越麻烦,成本很大,那么这个时候如果使用钩子机制,该如何做呢?
// 新的类型只需要在钩子里添加
var typeHook = {
'Array': 'array',
'Object': 'object',
'String': 'string',
'Date': 'date'
...
}
function type (obj) {
var t = Object.prototype.toString.call(obj).slice(8, -1);
typeHook[t];
}
可以看到,使用钩子去处理多种情况,可以让代码的逻辑更加清晰,省去大量的条件判断,上面的钩子机制的实现方式,采用的就是表驱动方式,就是我们事先预定好一张表(俗称打表),用这张表去适配多种情况。我们来看看 jquery 的 type 判断怎么写的。
(function(window, undefined) {
var
// 用于预存储一张类型表用于 hook
class2type = {};
// 针对一些特殊的对象(例如 null,Array,RegExp)也进行精准的类型判断
// 运用了钩子机制,判断类型前,将常见类型打表,先存于一个 Hash 表 class2type 里边
jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
class2type["[object " + name + "]"] = name.toLowerCase();
});
jQuery.extend({
// 确定JavaScript 对象的类型
type: function(obj) {
if (obj == null) {
return String(obj);
}
// 利用事先存好的 hash 表 class2type 作精准判断
// 这里因为 hook 的存在,省去了大量的 else if 判断
return typeof obj === "object" || typeof obj === "function" ?
class2type[core_toString.call(obj)] || "object" :
typeof obj;
}
})
//class2type表是这样的
{
"[object Array]": "array",
"[object Boolean]": "boolean",
"[object Date]": "date",
"[object Error]": "error",
"[object Function]": "function",
"[object Number]": "number",
"[object Object]": "object",
"[object RegExp]": "regexp",
"[object String]": "string"
}
})(window);
上述每个分支其实是从上到下依次判断,最后仅走入其中一个,对于这种简单的判定没有问题。如果每个 else if 分支都包含了复杂的条件判断,且其对执行的先后顺序有所要求。这时我们可以用钩子函数来解决。
var hook = {
get: function(elem) {
return "something";
},
set: function(elem, value) {
// do something with value
}
}
流程图如下:
从某种程度上讲,钩子是一系列被设计为以你自己的代码来处理自定义值的回调函数。有了钩子,你可以将差不多任何东西保持在可控范围内。从设计模式的角度而言,这种钩子运用了策略模式。
策略模式:将不变的部分和变化的部分隔开是每个设计模式的主题,而策略模式则是将算法的使用与算法的实现分离开来的典型代表。使用策略模式重构代码,可以消除程序中大片的条件分支语句。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列的“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以使用策略模式来封装他们。
策略模式的优点:
JavaScript 中所有的函数默认都有一个名为 prototype(原型)的公有并且不可枚举的属性,这个属性是一个指针,它会指向一个对象。
function Foo () {
}
Foo.prototype; //{ }
而这个对象通常被称为Foo的原型对象,用 FooPrototype(自己假想的)表示。 Foo.prototype = FooPrototype; 在默认情况下,所有原型对象都会获得一个 constructor 属性,这个属性包含一个指向prototype属性所在函数的指针。当我们使用 new 来调用这个函数Foo创建一个实例时,该实例的内部会包含一个指针(内部属性),指向这个构造函数的原型对象,这个指针叫 [[prototype]] ,可以使用 proto 来访问这个原型对象。
function Foo () {
}
Foo.__proto__ === Function.prototype // true
var a = new Foo();
a.__proto__ === Foo.prototype; // true
Object.getPrototypeOf(a) === Foo.prototype; // true
Foo.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null;// true
Foo.prototype.constructor === Foo; // true
a.constructor === Foo; // true
a.constructor === Foo.prototype.constructor; // true
这里有个比较疑惑的点在于 a.constructor ,难道 a 也有 .constructor 属性?实际上a本身并没有 .constructor 属性,而且,虽然 a.constructor 确实指向 Foo 函数,但是这个属性并不是表示 a 有 Foo “构造”的。
实际上,.constructor 引用同样被委托给了 Foo.prototype ,而 Foo.prototype.constructor 默认指向 Foo 。
举个例子:
function Foo () {
}
Foo.prototype = { /**/ };//创建一个新原型对象,相当于 new Object()。
var a1 = new Foo();
a1.constructor === Foo;//false
a1.constructor === Object.prototype.constructor;//true
a1.constructor === Object;//true
a1并没有 .constructor 属性,所以它会委托 [[prototype]] 链上的 Foo.prototype 。但是这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这个属性!),所以它会继续委托,这次它会委托给委托链顶端的 Object.prototype 。这个对象有 .constructor 属性,指向内置的 Object(...) 函数。
当然,我们可以给 Foo.prototype 添加一个 .constructor 属性,这需要手动添加一个符合正常行为的不可枚举属性。
function Foo () {
}
Foo.prototype = { /**/ };
Object.defineProperty({
enumerable: false,
writable: true,
configurable: true,
value: Foo // 让 .constructor 指向 Foo
})
JavaScript 里其他的引用类型,如 Array ,Date ,RegExp ,Function ,String ,Number ,Boolean 等。举例如下:
var a = new Array();
a.__proto__ === Array.prototype; // true
a.__proto__.__proto__ === Object.prototype; // true
a.__proto__.__proto__.__proto__ === null; // true
Array.prototype.constructor === Array;//true
a.constructor === Array;//true
a.constructor === Array.prototype.constructor;//true
Array.constructor === Function;// true
字典存储的是[键,值]对,其中键名是用来查询特定元素的。字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。字典也称映射。示例代码如下:
function Dictionary(){
var items = {};
this.set = function(key, value){
items[key] = value;
};
this.remove = function(key){
if (this.has(key)){
delete items[key];
return true;
}
return false;
};
this.has = function(key){
return items.hasOwnProperty(key);
};
this.get = function(key) {
return this.has(key) ? items[key] : undefined;
};
this.clear = function(){
items = {};
};
this.size = function(){
return Object.keys(items).length;
};
this.keys = function(){
return Object.keys(items);
};
this.values = function(){
var values = [];
for (var k in items) {
if (this.has(k)) {
values.push(items[k]);
}
}
return values;
};
this.each = function(fn) {
for (var k in items) {
if (this.has(k)) {
fn(k, items[k]);
}
}
};
this.getItems = function(){
return items;
}
}
散列表即HashTable类,也叫HashMap类,是Dictionary类的一种散列实现方式。散列算法的作用是尽可能的在数据结构中找到一个值。在以前的系列中,如果要在数据结构中获取一个值,需要遍历整个数据结构来找到它。如果使用散列函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后返回值在表中的位置。示例如下:
function HashTable() {
var table = [];
var loseloseHashCode = function (key) { //(1)散列函数
var hash = 0;
for (var i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37;
};
var djb2HashCode = function (key) { //(2)散列函数
var hash = 5381;
for (var i = 0; i < key.length; i++) {
hash = hash * 33 + key.charCodeAt(i);
}
return hash % 1013;
};
var hashCode = function (key) {
return loseloseHashCode(key);
};
this.put = function (key, value) { //根据所给的key通过散列函数计算出它在表中的位置,进而作映射
var position = hashCode(key);
console.log(position + ' - ' + key);
table[position] = value;
};
this.get = function (key) {
return table[hashCode(key)];
};
this.remove = function(key){
table[hashCode(key)] = undefined;
};
this.print = function () {
for (var i = 0; i < table.length; ++i) {
if (table[i] !== undefined) {
console.log(i + ": " + table[i]);
}
}
};
}
有时候一些键会有相同的键值。不同的的值在散列表中对应相同位置的时候,我们称其为冲突。此时,当我们通过相同的散列值去取属性值的时候会出现相互覆盖、数据丢失的情况。处理冲突有几种方法:分离链接,线性探查和双散列法,这里介绍前两种。
分离链接法包括为散列表的每个位置创建一个链表并将元素存储在里面。示例代码如下:
function HashTableSeparateChaining(){
var table = [];
var ValuePair = function(key, value){ //新的辅助类来加入LinkedList实例的元素,用到之前的链表
this.key = key;
this.value = value;
this.toString = function() {
return '[' + this.key + ' - ' + this.value + ']';
}
};
var loseloseHashCode = function (key) { //散列函数得出一个散列值key
var hash = 0;
for (var i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37;
};
var hashCode = function(key){
return loseloseHashCode(key);
};
this.put = function(key, value){
var position = hashCode(key);
console.log(position + ' - ' + key);
if (table[position] == undefined) { //判断是否被占据了
table[position] = new LinkedList();
}
table[position].append(new ValuePair(key, value)); //LinkedList实例中添加一个ValuePair实例
};
this.get = function(key) {
var position = hashCode(key);
if (table[position] !== undefined && !table[position].isEmpty()){
var current = table[position].getHead();
while(current.next){ //遍历链表来寻找键值
if (current.element.key === key){
return current.element.value;
}
current = current.next;
}
//检查元素在链表第一个或最后一个节点的情况
if (current.element.key === key){
return current.element.value;
}
}
return undefined;
};
this.remove = function(key){
var position = hashCode(key);
if (table[position] !== undefined){
var current = table[position].getHead();
while(current.next){ //遍历
if (current.element.key === key){
table[position].remove(current.element);
if (table[position].isEmpty()){
table[position] = undefined;
}
return true;
}
current = current.next;
}
//检查元素在链表第一个或最后一个节点的情况
if (current.element.key === key){
table[position].remove(current.element);
if (table[position].isEmpty()){
table[position] = undefined;
}
return true;
}
}
return false;
};
this.print = function() {
for (var i = 0; i < table.length; ++i) {
if (table[i] !== undefined) {
console.log(table[i].toString());
}
}
};
}
当想向表中某个位置加入一个新元素的时候,如果索引为index的位置已经被占据了,就尝试index+1的位置。如果index+1的位置也被占据了,就尝试index+2的位置,以此类推。示例代码如下:
function HashLinearProbing(){
var table = [];
var ValuePair = function(key, value){
this.key = key;
this.value = value;
this.toString = function() {
return '[' + this.key + ' - ' + this.value + ']';
}
};
var loseloseHashCode = function (key) {
var hash = 0;
for (var i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37;
};
var hashCode = function(key){
return loseloseHashCode(key);
};
this.put = function(key, value){
var position = hashCode(key);
console.log(position + ' - ' + key);
if (table[position] == undefined) { //如果没有元素存在加入
table[position] = new ValuePair(key, value);
} else {
var index = ++position;
while (table[index] != undefined){ //有的话继续往后找,直到找到加入
index++;
}
table[index] = new ValuePair(key, value);
}
};
this.get = function(key) {
var position = hashCode(key);
if (table[position] !== undefined){
if (table[position].key === key) {
return table[position].value;
} else {
var index = ++position;
while (table[index] === undefined || table[index].key !== key){ //循环迭代
index++;
}
if (table[index].key === key) { //验证key
return table[index].value;
}
}
}
return undefined;
};
this.remove = function(key){
var position = hashCode(key);
if (table[position] !== undefined){
if (table[position].key === key) {
table[position] = undefined;
} else {
var index = ++position;
while (table[index] === undefined || table[index].key !== key){
index++;
}
if (table[index].key === key) {
table[index] = undefined;
}
}
}
};
this.print = function() {
for (var i = 0; i < table.length; ++i) {
if (table[i] !== undefined) {
console.log(i + ' -> ' + table[i].toString());
}
}
};
}
冒泡排序比较任何两个相邻的项,如果第一个项比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。
function ArrayList(){
var array = [];
this.insert = function(item){
array.push(item);
};
var swap = function(index1, index2){ //交换值
var aux = array[index1];
array[index1] = array[index2];
array[index2] = aux;
};
this.toString= function(){
return array.join();
};
this.bubbleSort = function(){ // 常规的冒泡排序
var length = array.length;
for (var i=0; i<length; i++){
for (var j=0; j<length-1; j++ ){ // 内循环迭代至倒数第二位
if (array[j] > array[j+1]){
swap(j, j+1);
}
}
}
};
this.modifiedBubbleSort = function(){ //改进后的冒泡排序,避免内循环不必要的比较
var length = array.length;
for (var i=0; i<length; i++){
for (var j=0; j<length-1-i; j++ ){ //减去最后一个已经排好序的位置
if (array[j] > array[j+1]){
swap(j, j+1);
}
}
}
};
}
选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值并将其放在第一位,接着找到第二小的值并将其放在第二位,以此类推。
this.selectionSort = function(){
var length = array.length,
indexMin;
for (var i=0; i<length-1; i++){
indexMin = i;
for (var j=i; j<length; j++){ // 内循环记录array[i]后面最小的index位置
if(array[indexMin]>array[j]){
indexMin = j;
}
}
if (i !== indexMin){
swap(i, indexMin);
}
}
};
插入排序每次排一个数组项,假定第一项已经排好序了,接着,它和第二项进行比较,如果第二项比第一项大则待在原位,否则插入到第一项之前,以此类推。
this.insertionSort = function(){
var length = array.length,
j, temp;
for (var i=1; i<length; i++){ //假设第一个位置已经排好位置了
j = i;
temp = array[i];
while (j>0 && array[j-1] > temp){ //数组前面的比temp大
array[j] = array[j-1]; //把这个值移到当前位置
j--;
}
array[j] = temp;
}
};
归并排序是第一个可以被实际使用的排序算法,前面的三个排序算法的时间复杂度度为O(n²),但是这个归并排序性能不错,其时间复杂度为O(nlogⁿ)。归并排序是一种分治算法。其**是将原始数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。
this.mergeSort = function(){
array = mergeSortRec(array);
};
var mergeSortRec = function(array){
var length = array.length;
if(length === 1) { //切割数组直到只有一个元素
return array;
}
var mid = Math.floor(length / 2), //分层两边,左和右
left = array.slice(0, mid),
right = array.slice(mid, length);
return merge(mergeSortRec(left), mergeSortRec(right)); //递归函数
};
var merge = function(left, right){
var result = [],
il = 0,
ir = 0;
while(il < left.length && ir < right.length) { //决定是谁先入数组,小的先入
if(left[il] < right[ir]) {
result.push(left[il++]);
} else{
result.push(right[ir++]);
}
}
while (il < left.length){ //大的后入
result.push(left[il++]);
}
while (ir < right.length){ //大的后入
result.push(right[ir++]);
}
return result;
};
快速排序也许是最常用的排序算法了,它的时间复杂度为O(nlogⁿ),且它的性能通常比其他的复杂度O(nlogⁿ)的排序算法要好。和归并排序一样,快速排序也使用分治的方法,将原始数组分为较小的数组(但它没有像归并排序那样将他们分割开)。
this.quickSort = function(){
quick(array, 0, array.length - 1);
};
var partition = function(array, left, right) {
var pivot = array[Math.floor((right + left) / 2)], //选择一个中间值
i = left,
j = right;
while (i <= j) {
while (array[i] < pivot) { //移动指针直到找到一个元素比中间值大
i++;
}
while (array[j] > pivot) { //移动指针直到找到一个元素比中间值小
j--;
}
if (i <= j) { //左项比右项大,交换后同时移动两个指针
swapQuickStort(array, i, j);
i++;
j--;
}
}
return i;
};
var swapQuickStort = function(array, index1, index2){
var aux = array[index1];
array[index1] = array[index2];
array[index2] = aux;
};
var quick = function(array, left, right){
var index; //用来将子数组分离为较小值数组和较大值数组
if (array.length > 1) {
index = partition(array, left, right); //划分数组
if (left < index - 1) { //如果子数组存在较小值的元素,递归
quick(array, left, index - 1);
}
if (index < right) { ////如果子数组存在较大值的元素,递归
quick(array, index, right);
}
}
return array;
};
顺序或线性搜索是最基本的搜索算法。它的机制是,将一个数据结构中的元素和我们要找的元素做比较。顺序搜索是最低效的一种搜索算法。
this.sequentialSearch = function(item){
for (var i=0; i<array.length; i++){
if (item === array[i]){
return i;
}
}
return -1;
};
这个算法要求被搜索的数据结构已排序。以下是该算法遵循的步骤:
选择数组的中间值
如果选中值是待搜索值,那么算法执行完毕。
如果待搜索值比选中值要小,则返回步骤1并在选中值左边的子数组中寻找。
如果待搜索值比选中值要大,则返回步骤1并在选中值右边的子数组中寻找。
this.binarySearch = function(item){
this.quickSort();
var low = 0,
high = array.length - 1,
mid, element;
while (low <= high){
mid = Math.floor((low + high) / 2);
element = array[mid];
if (element < item) {
low = mid + 1;
} else if (element > item) {
high = mid - 1;
} else {
return mid;
}
}
return -1;
};
树是一种分层数据的抽象模型。一个树的结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点。
二叉树中的节点最多只能有两个节点:一个是左侧子节点,另一个是右侧子节点。二叉搜索树(BST)是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大(或者等于)的值。下面示例(BST)的代码:
function BinarySearchTree() {
var Node = function(key){ //数据结构类
this.key = key;
this.left = null;
this.right = null;
};
var root = null; //根节点
this.insert = function(key){ //插入新的键
var newNode = new Node(key);
//special case - first element
if (root === null){ //根节点为空,作为根节点
root = newNode;
} else {
insertNode(root,newNode); //插入节点操作
}
};
var insertNode = function(node, newNode){
if (newNode.key < node.key){
if (node.left === null){ //如果没有左侧节点就插入新的节点
node.left = newNode;
} else { //有的话递归
insertNode(node.left, newNode);
}
} else {
if (node.right === null){ //如果没有右侧节点就插入新的节点
node.right = newNode;
} else { //有的话递归
insertNode(node.right, newNode);
}
}
};
this.getRoot = function(){
return root;
};
this.search = function(key){ //搜索键
return searchNode(root, key); //搜索操作
};
var searchNode = function(node, key){
if (node === null){
return false;
}
if (key < node.key){ //如果小于继续从左边搜索
return searchNode(node.left, key);
} else if (key > node.key){ //如果大于继续从右边搜索
return searchNode(node.right, key);
} else { //命中
return true;
}
};
this.min = function() { //找最小键
return minNode(root);
};
var minNode = function (node) {
if (node){
while (node && node.left !== null) {
node = node.left;
}
return node.key;
}
return null;
};
this.max = function() { //找最大键
return maxNode(root);
};
var maxNode = function (node) {
if (node){
while (node && node.right !== null) {
node = node.right;
}
return node.key;
}
return null;
};
this.remove = function(element){
root = removeNode(root, element);
};
var findMinNode = function(node){ //返回节点
while (node && node.left !== null) {
node = node.left;
}
return node;
};
var removeNode = function(node, element){ //移除一个节点
if (node === null){
return null;
}
if (element < node.key){
node.left = removeNode(node.left, element);
return node;
} else if (element > node.key){
node.right = removeNode(node.right, element);
return node;
} else { //命中后分三种情况
//移除叶子节点,即该节点没有左侧或者右侧子节点的叶结点
if (node.left === null && node.right === null){
node = null;
return node;
}
//移除有一个左侧或者右侧子节点的节点
if (node.left === null){
node = node.right; //把引用改为子节点的引用,下同
return node;
} else if (node.right === null){
node = node.left;
return node;
}
//移除有两个子节点的节点
var aux = findMinNode(node.right); //找到右边子树的最小节点
node.key = aux.key; //改变节点的键,更新节点的值
node.right = removeNode(node.right, aux.key); //移除有相同键的节点
return node; //返回更新后节点的引用
}
};
}
两个月前做横幅特效的需求的时候,产品体验玻璃水滴的两种特效会导致网站整体变卡,之前没查出原因,就先下架了。最近潜心研究了下谷歌的工具,于是,把之前的两种特效特意拿出来研究下,把卡的问题解决,并上线,毕竟这两种特效还是不错的!
网页卡常见原因:DOM操作频繁,频繁触发回流,循环耗时等。当然,大多数小问题导致的问题在网站的整体效果上看可能会感知不到,而当你感觉到卡的时候肯定是“出事了”,下面我们将通过谷歌工具把卡的问题找出来。
我们已最新版的谷歌为例子,截图如下:
1、在DevTools中,单击 Performance tab。
2、勾选Screenshots checkbox。
3、点击录制设置按钮。DevTools显示与捕获性能指标的相关设置。
4、对于CPU,请选择2x减速。 DevTools控制CPU,使其比平常慢2倍(两倍看不出问题就5倍)。当然这个看情况而定,目的是节流cpu,暴露问题。
5、上面操作后整体截图如下(注意画箭头的地方):
1、在DevTools中,单击记录。 DevTools在页面运行时捕获性能指标。
2、等待数秒。
3、单击停止。 DevTools停止录制,处理数据,然后在“性能”面板上显示结果。如下图:
1、看看FPS图表。
每当您看到FPS上方的红色条纹,这意味着帧率下降得如此之低,以致可能会损害用户体验。 一般来说,绿色条越高,FPS越高。如下图:
见红的那部分fps帧率很低,表示有问题。
2、看看CPU图表。
在FPS图表下方,可以看到CPU图表。CPU图表中的颜色对应于“性能”面板。底部“摘要”选项卡中的颜色。 CPU图表充满色彩意味着CPU在录制过程中被最大化。如下图:
3、时间点截图。
将鼠标悬停在FPS,CPU或NET图表上。 DevTools在该时间点显示页面的屏幕截图。如下图:
4、发现性能瓶颈。
在时间轴上选中部分见红的部分(即fps很低的部分),展开“main”主线程。 DevTools随着时间的推移,显示主线程上的活动的火焰图表。 随着时间的推移,x轴表示录制。 每个格代表一个事件。 更宽的格意味着事件花费更长时间。 y轴表示调用堆栈。如下图:
从上图可以很明显看出,“Animation Frame Fired”的格的右上角又见“红”了,说明有问题,点击这个“格”。如下图所示:
谷歌工具已经看不下去了,给了个提示,Warning Recurring handler took 131.66 ms,循环导致的问题。再往下,点击具体的文件行数,可以定位出现问题的位置。如下图:
5、解决问题,该你了。
最近帮公司网站banner实现了几个动画特效,踩了一些坑,这里mark下,下面给个流星demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas梦幻星空背景</title>
<style>
*{margin: 0;padding: 0;}
html,body{height: 100%;}
body{background: rgba(0, 0, 0, 0.9);}
</style>
</head>
<body>
<script>
(function () {
var context;//画布上下文
var boundaryHeight;//画布高,边界值
var boundaryWidth;//画布宽,边界值
var starArr = [];
var meteorArr = [];
var STAR_COUNT = 500;//星星数,常量
var METEOR_COUNT = 4;//流星数,常量
var METEOR_SEPARATE = 300; //流星之间间隔,常量
var meteorCoordinate = [];//存所以流星的坐标
var playMeteorTimeout;
var playStarsTimeout;
//初始化画布及context
function init(container) {
starArr = [];
meteorArr = [];
var canvas = document.createElement("canvas");
container.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
boundaryHeight = canvas.height;
boundaryWidth = canvas.width;
//获取context
context = canvas.getContext("2d");
context.fillStyle = "black";
//画星星
for (var i = 0; i < STAR_COUNT; i++) {
var star = new Star();
star.init();
star.draw();
starArr.push(star);
}
//画流星
for (var j = 0; j < METEOR_COUNT; j++) {
var rain = new MeteorRain();
rain.init(j);
rain.draw();
meteorArr.push(rain);
}
playStars();
playMeteor();
}
//创建一个星星对象
var Star = function () {
this.x = boundaryWidth * Math.random();//横坐标
this.y = boundaryHeight * Math.random();//纵坐标
this.color = "";//星星颜色
};
Star.prototype = {
constructor: Star,
//初始化
init: function() {
this.getColor();
},
//产生随机颜色
getColor: function() {
var _randomNum = Math.random();
if (_randomNum < 0.5) {
this.color = "gray";
}
else {
this.color = "white";
}
},
//绘制
draw: function() {
context.beginPath();
//画圆点
context.arc(this.x, this.y, 0.05, 0, 2 * Math.PI);
context.strokeStyle = this.color;
context.stroke();
context.closePath();
}
}
//星星闪起来
function playStars() {
for (var n = 0; n < STAR_COUNT; n++) {
starArr[n].getColor();
starArr[n].draw();
}
clearTimeout(playStarsTimeout);
playStarsTimeout = setTimeout(playStars, 200);
}
//创建一个流星对象
var MeteorRain = function () {
this.x = -1;
this.y = -1;
this.length = -1;//长度
this.angle = 30; //倾斜角度
this.width = -1;//流星所占宽度
this.height = -1;//流星所占高度
this.speed = 1;//速度
this.offset_x = -1;//横轴移动偏移量
this.offset_y = -1;//纵轴移动偏移量
this.alpha = 1; //透明度
}
MeteorRain.prototype = {
constructor: MeteorRain,
//初始化
init: function (i) {
this.alpha = 1;//透明度
this.angle = 30; //流星倾斜角
this.speed = Math.ceil(Math.random() + 0.5); //流星的速度
var x = Math.random() * 80 + 180;
var cos = Math.cos(this.angle * 3.14 / 180);
var sin = Math.sin(this.angle * 3.14 / 180) ;
this.length = Math.ceil(x);//流星长度
this.width = this.length * cos; //流星所占宽度,及矩形的宽度
this.height = this.length * sin; //流星所占高度,及矩形的高度
this.offset_x = this.speed * cos * 3.5;
this.offset_y = this.speed * sin * 3.5;
this.getPos(i);
},
//计算流星坐标
countPos: function () {
//往左下移动,x减少,y增加
this.x = this.x - this.offset_x;
this.y = this.y + this.offset_y;
},
//获取随机坐标
getPos: function (i) {
_this = this;
function getCoordinate() {
_this.x = Math.random() * boundaryWidth; //x坐标
for (var k = 0; k < meteorCoordinate.length; k++) {
if (Math.abs(_this.x - meteorCoordinate[k]) < METEOR_SEPARATE) { //这里如果流星之间距离过小,会把其他流星隔断,严重影响效果。
return getCoordinate();
}
}
meteorCoordinate[i] = _this.x;
}
getCoordinate();
this.y = 0.2 * boundaryHeight; //y坐标
},
//画流星
draw: function () {
context.save();
context.beginPath();
context.lineWidth = 2.5; //宽度
context.globalAlpha = this.alpha; //设置透明度
//创建横向渐变颜色,起点坐标至终点坐标
var line = context.createLinearGradient(this.x, this.y, this.x + this.width, this.y - this.height);
//分段设置颜色
line.addColorStop(0, "rgba(255, 255, 255, 1)");
line.addColorStop(1, "rgba(255, 255,255 , 0)");
if (this.alpha < 0 ) {
this.alpha = -this.alpha;
}
//填充
context.strokeStyle = line;
//起点
context.moveTo(this.x, this.y);
//终点
context.lineTo(this.x + this.width, this.y - this.height);
context.closePath();
context.stroke();
context.restore();
},
move: function () {
var x = this.x + this.width - this.offset_x;
var y = this.y - this.height;
this.alpha -= 0.002;
//重新计算位置,往左下移动
this.countPos();
if (this.alpha <= 0) {
this.alpha = 0;
}
else if(this.alpha > 1) {
this.alpha = 1;
}
//画一个矩形去清空流星
context.clearRect(this.x - this.offset_x, y, this.width + this.offset_x, this.height);
//重绘
this.draw();
}
}
//流星动起来
function playMeteor() {
for (var n = 0; n < METEOR_COUNT; n++) {
var rain = meteorArr[n];
rain.move();//移动
if (rain.y > boundaryHeight + 100) {//超出界限后重来
context.clearRect(rain.x, rain.y - rain.height, rain.width, rain.height);
meteorCoordinate[n] = 0;//清空数组坐标具体流星的坐标
meteorArr[n] = new MeteorRain(n);
meteorArr[n].init(n);
}
}
clearTimeout(playMeteorTimeout);
playMeteorTimeout = setTimeout(playMeteor, 5);
}
init(document.getElementsByTagName("body")[0]);
}());
效果如下:
没有名称的按钮对于依赖屏幕阅读器的用户不可用。
建议:
对于 元素和具有 role = "button" 的元素:
<button id="text">Name</button>
<button id="al" aria-label="Name"></button>
<button id="alb" aria-labelledby="labeldiv"></button>
<div id="labeldiv">Button label</div>
对于 的元素:
对于 和 :
文档标题提供页面用途的概述。对于依靠屏幕阅读器的用户而言,标题尤其重要,因为他们对页面的了解较少。
建议:
标签阐明了表单元素的用途。尽管每个元素对于有视力的用户而言都是显而易见的,但对于依赖屏幕阅读器的用户而言,情况往往并非如此。
将标签与每个表单元素相关联。有四种方法可以这样做:
<label>First Name <input type="text"/></label>
<label for="first">First Name <input type="text" id="first"/></label>
<button class="hamburger-menu" aria-label="menu">...</button>
<span id="foo">Select seat:</span>
<custom-dropdown aria-labelledby="foo">...</custom-dropdown>
信息性图像应具有 alt 属性,该属性应包含该图像内容的文本描述。 屏幕阅读器通过将文本内容转换为他们可以使用的形式(例如合成语音或盲文),使视障用户可以使用您的网站。 屏幕阅读器无法转换图像。 因此,如果您的图像包含重要信息,则视障用户将无法访问该信息。
可以在 DevTools 的 Console 选项卡中使用以下命令来查找没有 alt 属性的图片
$$('img:not([alt])');
tabindex 属性使元素可以通过键盘导航。正值表示元素的明确导航顺序。 尽管这是有效的,但实际上很难正确地进行操作,并且会给依赖辅助技术的用户带来无法使用的体验。
有关更多信息,请参见 使用 tabindex。
将这些元素中的每一个的 tabindex 设置为 -1(对于不应通过键盘导航的元素)或 0(对于应通过键盘进行导航的元素)。 如果您需要元素在 Tab 键顺序中更早出现,请考虑将在 DOM 中去移动他们的顺序。
图是网络结构的抽象模型。图是一组由边连接的节点,任何二元关系都可以用图来表示。
一个图G = (V,E)由以下元素组成。
下图表示一个图:
由一条边连接在一起的顶点称为相邻顶点。比如上图的A和B是相邻的,A和D是相邻的,A和C是相邻的,A和E不是相邻的。一个顶点的度是其相邻顶点的数量。比如,A和其他三个顶点相连接,因此,A的度为3;E和其他两个顶点相连,因此E的度为2。路径是顶点v1,v2,...,vk的一个连续序列,其中v[i]和v[i+1]是相邻的。以上的图为例,其中的路径有ABEI和ACDG。
图最常见的实现是邻接矩阵。每个节点都和一个整数相关联,该整数作为数组的索引。这里不作讨论。另一种图的表示方式是一种叫做邻接表的动态数据结构。邻接表由图中每个顶点的相邻顶点列表所组成。我们可以用数组,链表,甚至是散列表或是字典来表示相邻顶点列表。下面的示意图展示了邻接表的数据结构。我们后面也会用代码示例这种数据结构。
示例代码如下:
function Graph() {
var vertices = []; //存储图中所有的顶点名字
var adjList = new Dictionary();//用之前的一个字典来存储邻接表
this.addVertex = function(v){ //添加顶点
vertices.push(v);
adjList.set(v, []); //顶点为键,字典值为空数组
};
this.addEdge = function(v, w){ //添加边
adjList.get(v).push(w); //基于有向图
adjList.get(w).push(v); //基于无向图
};
this.toString = function(){
var s = '';
for (var i=0; i<vertices.length; i++){
s += vertices[i] + ' -> ';
var neighbors = adjList.get(vertices[i]);
for (var j=0; j<neighbors.length; j++){
s += neighbors[j] + ' ';
}
s += '\n';
}
return s;
};
var initializeColor = function(){
var color = [];
for (var i=0; i<vertices.length; i++){
color[vertices[i]] = 'white';
}
return color;
};
}
//测试
var graph = new Graph();
var myVertices = ['A','B','C','D','E','F','G','H','I'];
for (var i=0; i<myVertices.length; i++){
graph.addVertex(myVertices[i]);
}
graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'G');
graph.addEdge('D', 'G');
graph.addEdge('D', 'H');
graph.addEdge('B', 'E');
graph.addEdge('B', 'F');
graph.addEdge('E', 'I');
console.log(graph.toString());
结果如下:
A -> B C D
B -> A E F
C -> A D G
D -> A C G H
E -> B I
F -> B
G -> C D
H -> D
I -> E
和树的数据结构类似,我们可以访问图的所有节点。有两种算法可以对图进行遍历:广度优先搜索(Breadth-First Search,BFS)和深度优先搜索(Depth-First Search,DFS)。图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环等。
图遍历算法的**是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。完全探索一个顶点要求我们查看该顶点的每一条边。对应每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。
为了保证算法的效率,务必访问每个顶点至多两次。连通图中每条边和顶点都会被访问到。当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态:
广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深的访问顶点。以下是从顶点v开始的广度优先搜索算法所遵循的步骤。
示例代码如下:
var initializeColor = function(){
var color = [];
for (var i=0; i<vertices.length; i++){
color[vertices[i]] = 'white'; //初始化所有的顶点都是白色
}
return color;
};
this.bfs = function(v, callback){
var color = initializeColor(),
queue = new Queue(); //创建一个队列
queue.enqueue(v); //入队列
while (!queue.isEmpty()){
var u = queue.dequeue(), //出队列
neighbors = adjList.get(u); //邻接表
color[u] = 'grey'; //发现了但还未完成对其的搜素
for (var i=0; i<neighbors.length; i++){
var w = neighbors[i]; //顶点名
if (color[w] === 'white'){
color[w] = 'grey'; //发现了它
queue.enqueue(w); //入队列循环
}
}
color[u] = 'black'; //已搜索过
if (callback) {
callback(u);
}
}
};
//测试如下:
function printNode(value){
console.log('Visited vertex: ' + value);
}
graph.bfs(myVertices[0], printNode);
结果如下:
Visited vertex: A
Visited vertex: B
Visited vertex: C
Visited vertex: D
Visited vertex: E
Visited vertex: F
Visited vertex: G
Visited vertex: H
Visited vertex: I
深度优先搜索算法将会是从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点。深度优先搜索算法不需要一个源顶点。要访问顶点v,照如下的步骤做:
如你所见,深度优先搜索的步骤是递归的,这意味着深度优先搜索算法使用栈来存储函数调用(由递归调用所创建的栈)。示例代码如下:
this.dfs = function(callback){
var color = initializeColor(); //前面的颜色数组
for (var i=0; i<vertices.length; i++){
if (color[vertices[i]] === 'white'){
dfsVisit(vertices[i], color, callback); //递归调用未被访问过的顶点
}
}
};
var dfsVisit = function(u, color, callback){
color[u] = 'grey';
if (callback) {
callback(u);
}
var neighbors = adjList.get(u); //邻接表
for (var i=0; i<neighbors.length; i++){
var w = neighbors[i];
if (color[w] === 'white'){
dfsVisit(w, color, callback); //添加顶点w入栈
}
}
color[u] = 'black';
};
//测试如下:
function printNode(value){
console.log('Visited vertex: ' + value);
}
graph.dfs(printNode);
结果如下:
Visited vertex: A
Visited vertex: B
Visited vertex: E
Visited vertex: I
Visited vertex: F
Visited vertex: C
Visited vertex: D
Visited vertex: G
Visited vertex: H
(1)基本类型:
5种基本数据类型Undefined、Null、Boolean、Number 和 String,变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问。
(2)引用类型:
存放在堆内存中的对象,变量保存的是一个指针,这个指针指向另一个位置。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。
JavaScript存储对象都是存地址的,所以浅拷贝会导致 obj1 和obj2 指向同一块内存地址。改变了其中一方的内容,都是在原来的内存上做修改会导致拷贝对象和源对象都发生改变,而深拷贝是开辟一块新的内存地址,将原对象的各个属性逐个复制进去。对拷贝对象和源对象各自的操作互不影响。
例如:数组拷贝
//浅拷贝,双向改变,指向同一片内存空间
var arr1 = [1, 2, 3];
var arr2 = arr1;
arr1[0] = 'change';
console.log('shallow copy: ' + arr1 + " ); //shallow copy: change,2,3
console.log('shallow copy: ' + arr2 + " ); //shallow copy: change,2,3
function shallowClone(copyObj) {
var obj = {};
for ( var i in copyObj) {
obj[i] = copyObj[i];
}
return obj;
}
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f); // true
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f); // true
Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在深拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝。原数组的元素会按照下述规则拷贝:
如果向两个数组任一中添加了新元素,则另一个不会受到影响。例子如下:
var array = [1,2,3];
var array_shallow = array;
var array_concat = array.concat();
var array_slice = array.slice(0);
console.log(array === array_shallow); //true
console.log(array === array_slice); //false,“看起来”像深拷贝
console.log(array === array_concat); //false,“看起来”像深拷贝
可以看出,concat和slice返回的不同的数组实例,这与直接的引用复制是不同的。而从另一个例子可以看出Array的concat和slice并不是真正的深复制,数组中的对象元素(Object,Array等)只是复制了引用。如下:
var array = [1, [1,2,3], {name:"array"}];
var array_concat = array.concat();
var array_slice = array.slice(0);
array_concat[1][0] = 5; //改变array_concat中数组元素的值
console.log(array[1]); //[5,2,3]
console.log(array_slice[1]); //[5,2,3]
array_slice[2].name = "array_slice"; //改变array_slice中对象元素的值
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice
JSON对象是ES5中引入的新的类型(支持的浏览器为IE8+),JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,借助这两个方法,也可以实现对象的深拷贝。
//例1
var source = { name:"source", child:{ name:"child" } }
var target = JSON.parse(JSON.stringify(source));
target.name = "target"; //改变target的name属性
console.log(source.name); //source
console.log(target.name); //target
target.child.name = "target child"; //改变target的child
console.log(source.child.name); //child
console.log(target.child.name); //target child
//例2
var source = { name:function(){console.log(1);}, child:{ name:"child" } }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
//例3
var source = { name:function(){console.log(1);}, child:new RegExp("e") }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
console.log(target.child); //Object {}
这种方法使用较为简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。还有一点不好的地方是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理。
jQuery的源码 - src/core.js #L121源码及分析如下:
jQuery.extend = jQuery.fn.extend = function() { //给jQuery对象和jQuery原型对象都添加了extend扩展方法
var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
//以上其中的变量:options是一个缓存变量,用来缓存arguments[i],name是用来接收将要被扩展对象的key,src改变之前target对象上每个key对应的value。
//copy传入对象上每个key对应的value,copyIsArray判定copy是否为一个数组,clone深拷贝中用来临时存对象或数组的src。
// 处理深拷贝的情况
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
//跳过布尔值和目标
i++;
}
// 控制当target不是object或者function的情况
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {};
}
// 当参数列表长度等于i的时候,扩展jQuery对象自身。
if (length === i) {
target = this; --i;
}
for (; i < length; i++) {
if ((options = arguments[i]) != null) {
// 扩展基础对象
for (name in options) {
src = target[name];
copy = options[name];
// 防止永无止境的循环,这里举个例子,
// 如 var a = {name : b};
// var b = {name : a}
// var c = $.extend(a, b);
// console.log(c);
// 如果没有这个判断变成可以无限展开的对象
// 加上这句判断结果是 {name: undefined}
if (target === copy) {
continue;
}
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是数组的话就让clone副本等于src否则等于空数组。
} else {
clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是对象的话就让clone副本等于src否则等于空数组。
}
// 递归拷贝
target[name] = jQuery.extend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy; // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性。
}
}
}
}
// 返回修改的对象
return target;
};
jQuery的extend方法使用基本的递归思路实现了浅拷贝和深拷贝,但是这个方法也无法处理源对象内部循环引用,例如:
var a = {"name":"aaa"};
var b = {"name":"bbb"};
a.child = b;
b.parent = a;
$.extend(true,{},a);//直接报了栈溢出。Uncaught RangeError: Maximum call stack size exceeded
(function ($) {
'use strict';
var types = 'Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' ');
function type () {
return Object.prototype.toString.call(this).slice(8, -1);
}
for (var i = types.length; i--;) {
$['is' + types[i]] = (function (self) {
return function (elem) {
return type.call(elem) === self;
};
})(types[i]);
}
return $;
})(window.$ || (window.$ = {}));//类型判断
function copy (obj,deep) {
if ($.isFunction(obj)) {
return new Function("return " + obj.toString())();
} else if (obj === null || (typeof obj !== "object")) {
return obj;
} else {
var name, target = $.isArray(obj) ? [] : {}, value;
for (name in obj) {
value = obj[name];
if (value === obj) {
continue;
}
if (deep) {
if ($.isArray(value) || $.isObject(value)) {
target[name] = copy(value,deep);
} else if ($.isFunction(value)) {
target[name] = new Function("return " + value.toString())();
} else {
target[name] = value;
}
} else {
target[name] = value;
}
}
return target;
}
}
节点元素描述对象的 type
属性用来标识一个节点的类型。
ast = {
type: 1
}
它有三个可取值,分别是 1
、2
、3
,分别代表的含义是:
1
:代表当前节点类型为标签2
:包含字面量表达式的文本节点3
:普通文本节点或注释节点当节点类型为 2
时,该节点的元素描述对象会包含 expression
属性。
ast = {
type: 2,
expression: "'abc'+_s(name)+'def'"
}
与 expression
类似,当节点类型为 2
时,该节点的元素描述对象会包含 tokens
属性。
ast = {
type: 2,
expression: "'abc'+_s(name)+'def'",
tokens: [
'abc',
{
'@binding': '_s(name)'
},
'def'
]
}
节点元素描述对象的 tokens
属性是用来给 weex
使用的,这里不做过多解释。
只有当节点类型为 1
,即该节点为标签时其元素描述对象才会有 tag
属性,该属性的值代表标签的名字。
ast = {
type: 1,
tag: 'div'
}
只有当节点类型为 1
,即该节点为标签时其元素描述对象才会有 attrsList
属性,它是一个对象数组,存储着原始的 html
属性名和值。
ast = {
type: 1,
attrsList: [
{
name: 'v-for',
value: 'obj of list'
},
{
name: 'class',
value: 'box'
}
]
}
节点元素描述对象的 attrsMap
属性与 attrsList
属性一样,不同点在于 attrsMap
是以键值对的方式保存 html
属性名和值的。
ast = {
type: 1,
attrsMap: {
'v-for': 'obj of list',
'class': 'box'
}
}
节点元素描述对象的 attrs
属性也是一个数组,并且也只有当节点类型为 1
,即节点为标签的时候,其元素描述对象才会包含这个属性。attrs
属性不同于 attrsList
属性,具体表现在:
attrsList
属性仅用于解析阶段,而 attrs
属性则用于代码生成阶段,甚至运行时阶段。attrsList
属性所包含的内容作为元素材料被解析器使用,而 attrs
属性所包含的内容在运行时阶段会使用原生 DOM
操作方法 setAttribute
真正将属性设置给 DOM
元素简单来说 attrs
属性会包含以下内容:
v-bind
(或其缩写:
) 指令绑定的属性会被添加到 attrs
数组中。为什么说大部分而不是全部呢?因为在 Vue
中有个 Must Use Prop
的概念,对于一个属性如果它是 Must Use Prop
的,则该属性不会被添加到 attrs
数组中,而是会被添加到元素描述对象的 props
数组中。
如下 html
模板所示:
<div :some-attr="val"></div>
最终 attrs
数组将为:
ast = {
attrs: [
{
name: 'some-attr',
value: 'val'
}
]
}
attrs
数组中。如下 html
模板所示:
<div no-binding-attr="val"></div>
最终 attrs
数组将为:
ast = {
attrs: [
{
name: 'no-binding-attr',
value: '"val"'
}
]
}
大家观察绑定属性和非绑定属性在 attrs
数组中的却别?很容易能够发现,非绑定属性的属性值是经过 JSON.stringify
的,我们已经不止一次的提到过这么做的目的。
slot
特性会被添加到 attrs
数组中。如下 html
模板所示:
<div slot="header"></div>
最终 attrs
数组将为:
ast = {
attrs: [
{
name: 'slot',
value: '"header"'
}
]
}
当然了由于 slot
本身是可绑定的属性,所以如果 html
模板如下:
<div :slot="header"></div>
最终 attrs
数组将为:
ast = {
attrs: [
{
name: 'slot',
value: 'header'
}
]
}
区别在于 value
值是非 JSON.stringify
化的。
实际上,并不是出现在 attrs
数组中的属性就一定会使用 setAttribute
函数将其添加到 DOM
上,例如在运行时阶段,组件会根据该组件自身的 props
定义,从 attrs
中抽离出那些作为组件 props
的属性元素。
节点元素描述对象的 props
属性也是一个数组,它的格式与 attrs
数组类似。就像 attrs
数组中的属性在运行时阶段会使用 setAttribute
函数将其添加到 DOM
上一样,props
数组中的属性则会直接通过 DOM
元素对象访问并添加,举个例子,假设 props
数组如下:
ast = {
props: [
{
name: 'innerHTML',
value: '"some text"'
}
]
}
则在运行时阶段,会使用如下代码操作 DOM
:
elm.innerHTML = 'some text'
其中 elm
为 DOM
节点对象。
那么那些属性会被当做 props
呢?有两种,第一种是在绑定属性时使用了 prop
修饰符,例如:
<div :some.prop="aaa"></div>
由于绑定 some
属性的时候使用了 prop
修饰符,所以 some
属性不会出现在元素描述对象的 attrs
数组中,而是会出现在元素描述对象的 props
数组中。
第二种是那些比较特殊的属性,在绑定这些属性时,即使没有指定 prop
修饰符,但是由于它属于 Must Use Prop
的,所以这些属性会被强制添加到元素描述对象的 props
数组中,只有那些属性是 Must Use Prop
,可以查看附录:mustuseprop
节点元素描述对象的 pre
属性是一个布尔值,它的真假代表着标签是否使用了 v-pre
指令,既然是标签,所以只有当节点的类型为 1
的时候其元素描述对象才会拥有 pre
属性。
ast = {
type: 1,
pre: true
}
标签的 Namespace
,如果一个标签是 SVG
标签,则该标签的元素描述对象将会拥有 ns
属性,其值为 'svg'
,如果一个标签是 <math>
标签,则该标签元素描述对象的 ns
属性值为字符串 'math'
。
ast = {
type: 1,
ns: 'svg'
}
节点元素描述对象的 forbidden
属性是一个布尔值,其真假代表着该节点是否是在 Vue
模板中禁止被使用的。在 Vue
模板中满足以下条件的标签为禁止使用的标签:
<style>
标签禁止出现在模板中。type
属性的 <script>
标签,或 type
属性值为 'text/javascript'
的 <script>
标签。节点元素描述对象的 parent
属性是父节点元素描述对象的引用。
节点元素描述对象的 children
属性是一个数组,存储着该节点所有子节点的元素描述对象。当然了有些节点是不可能拥有子节点的,比如普通文本节点,对于不可能拥有子节点的节点,其元素描述对象没有 children
属性。
ast = {
children: [
{
type: 1,
// 其他节点属性...
}
]
}
如果一个标签使用 v-if
指令,则该标签的元素描述对象将会拥有 ifConditions
属性,它是一个数组。如果一个标签使用 v-else-if
或 v-else
指令,则该标签不会被添加到其父节点元素描述对象的 children
数组中,而是会被添加到相符的带有 v-if
指令节点的元素描述对象的 ifConditions
数组中。
假设有如下模板:
<div v-if="a"></div>
<h1 v-else-if="b"></h1>
<p v-else></p>
则 <div>
标签元素描述对象将是:
ast = {
type: 1,
tag: 'div',
ifConditions: [
{
exp: 'a',
block: { type: 1, tag: 'div', ifConditions: [...] /* 省略其他属性 */ }
},
{
exp: 'b',
block: { type: 1, tag: 'h1' /* 省略其他属性 */ }
},
{
exp: undefined,
block: { type: 1, tag: 'p' /* 省略其他属性 */ }
}
],
// 其他属性...
}
可以发现一个节点元素描述对象的 ifConditions
数组中也会包含节点自身的元素描述对象。
只有 <slot>
标签的元素描述对象才会拥有 slotName
属性,代表该插槽的名字,假设模板如下:
<slot name="header" />
则元素描述对象为:
ast = {
type: 1,
tag: 'slot',
slotName: '"header"'
}
注意 <slot>
标签的 name
属性可以是绑定的:
<slot :name="dynamicName" />
则元素描述对象为:
ast = {
type: 1,
tag: 'slot',
slotName: 'dynamicName'
}
如果没有为 <slot>
标签指定 name
属性,则其元素描述对象的 slotName
属性为:
ast = {
type: 1,
tag: 'slot',
slotName: '""'
}
如果一个标签使用了 slot
特性,则说明该标签将会被作为插槽的内容,为了标识该标签将被插入的位置,该标签的元素描述对象会拥有 slotTarget
属性,假如有如下模板:
<div slot="header" ></div>
则其元素描述对象为:
ast = {
type: 1,
tag: 'div',
slotTarget: '"header"'
}
我们来对比一下使用 name
属性的 <slot>
标签的元素描述对象:
ast = {
type: 1,
tag: 'slot',
slotName: '"header"'
}
可以发现 slotTarget
和 slotName
是一一对象的,这将会在运行时阶段用来寻找合适的插槽内容。
另外 slot
特性也可以是绑定的:
<div :slot="dynamicTarger" ></div>
则其元素描述对象为:
ast = {
type: 1,
tag: 'div',
slotTarget: 'dynamicTarger'
}
如果没有为 slot
特性指定属性值,则该标签元素描述对象的 slotTarget
属性的值为:
ast = {
type: 1,
tag: 'div',
slotTarget: '"default"'
}
我们可以使用 slot-scope
特性来指定一个插槽内容是作用域插槽,此时该标签的元素描述对象将拥有 slotScope
属性,假如有如下模板:
<div slot-scope="scopeData"></div>
其元素描述对象为:
ast = {
type: 1,
tag: 'div',
slotScope: 'scopeData'
}
同常情况下我们插槽是作为一个组件的子节点去书写的,如下:
<comp>
<div slot="header"></div>
</comp>
如上代码所示我们有自定义组件 <copm>
,并为该自定义组件提供了插槽内容。普通插槽会出现在组件元素描述对象的 children
数组中,如下是以上模板的 AST
:
ast = {
type: '1',
tag: 'comp',
children: [
{
type: 1,
tag: 'div',
slotTarget: '"header"'
}
]
}
但如果一个插槽不是普通插槽,而是作用域插槽,则该插槽节点的元素描述对象不会作为组件的 children
属性存在,而是会被添加到组件元素描述对象的 scopedSlots
属性中,假设有如下模板:
<comp>
<div slot="header" slot-scope="scopeData"></div>
</comp>
则其生成的 AST
为:
ast = {
type: '1',
tag: 'comp',
children: [],
scopedSlots: {
'"header"': {
type: 1,
tag: 'div',
slotTarget: '"header"'
}
}
}
可以发现 scopedSlots
对象的键值是作用域插槽元素描述对象的 slotTarget
属性的值。
当标签使用了 v-for
指令时,其元素描述对象将会拥有以上这四个属性,在如上四个属性中,其中 for
、alias
这两个属性是肯定存在的,而 iterator1
和 iterator2
这两个属性不一定会存在。
如果模板如下:
<div v-for="obj of list"></div>
则其元素描述对象为:
ast = {
for: 'list',
alias: 'obj'
}
如果模板如下:
<div v-for="(obj, index) of list"></div>
则其元素描述对象为:
ast = {
for: 'list',
alias: 'obj',
iterator1: 'index'
}
如果模板如下:
<div v-for="(obj, key, index) of list"></div>
则其元素描述对象为:
ast = {
for: 'list',
alias: 'obj',
iterator1: 'key'
iterator2: 'index'
}
else
如果一个标签使用了 v-if
指令,则该标签元素描述对象就会拥有 if
属性,假如有如下模板:
<div v-if="a"></div>
则其元素描述对象为:
ast = {
if: 'a'
}
如果一个标签使用了 v-else-if
指令,则该标签元素描述对象就会拥有 elseif
属性,假如有如下模板:
<div v-else-if="b"></div>
则其元素描述对象为:
ast = {
elseif: 'b'
}
如果一个标签使用了 v-else
指令,则该标签元素描述对象就会拥有 else
属性,假如有如下模板:
<div v-else></div>
则其元素描述对象为:
ast = {
else: true
}
使用标签使用了 v-once
指令,则该标签的元素描述对象就会包含 once
属性,它是一个布尔值,如下:
ast = {
once: true
}
如果标签使用 key
特性,则该标签的元素描述对象就会包含 key
属性,假设有如下模板:
<div key="unique"></div>
则其元素描述对象为:
ast = {
key: '"unique"'
}
key
特性可以是绑定的:
<div :key="unique"></div>
则其元素描述对象为:
ast = {
key: 'unique'
}
与 key
类似,假设有如下模板:
<div ref="domRef"></div>
则其元素描述对象为:
ast = {
ref: '"domRef"'
}
ref
特性可以是绑定的:
<div :ref="domRef"></div>
则其元素描述对象为:
ast = {
ref: 'domRef'
}
元素描述对象的 refInFor
是一个布尔值。如果一个使用了 ref
特性的标签是使用了 v-for
指令标签的子代节点,则该标签元素描述对象的 checkInFor
属性将会为 true
,否则为 false
如果标签使用 is
特性,则其元素描述对象将会拥有 component
属性,假设有如下模板:
<component :is="currentView"></component>
则其元素描述对象为:
ast = {
type: 1,
tag: 'component',
component: 'currentView'
}
is
特性也可以是非绑定的:
<table></table>
<tr is="my-row"></tr>
</table>
则 <tr>
标签的元素描述对象为:
ast = {
type: 1,
tag: 'tr',
component: '"my-row"'
}
节点元素描述对象的 inlineTemplate
属性是一个布尔值,标识着一个组件使用使用内联模板,假设我们有如下模板:
<copm inline-template></copm>
则其元素描述对象为:
ast = {
inlineTemplate: true
}
节点元素描述对象的 hasBindings
属性是一个布尔值,用来标签当前节点是否拥有绑定,所谓绑定指的就是指令。所以如果一个标签使用了指令(包括自定义指令),则其元素描述对象的 hasBindings
属性就会为 true
。
这里要强调一点,事件本身也是指令(v-on
指令),绑定的属性也是指令(v-bind
指令)。
如果标签使用了 v-on
指令(或缩写 @
)绑定了事件,则该标签元素描述对象中将包含 events
属性,假如有如下模板:
<div @click="handleClick"></div>
则其元素描述对象为:
ast = {
events: {
'click': {
value: 'handleClick'
}
}
}
如果在绑定事件的时候使用了修饰符,如下模板所示:
<div @click.stop="handleClick"></div>
则其元素描述对象为:
ast = {
events: {
'click': {
value: 'handleClick',
modifiers: {
stop: true
}
}
}
}
可以看到多出了 modifiers
对象。
但并不是所有修饰符都会出现在 modifiers
对象中,如下模板所示:
<div @click.once="handleClick"></div>
如上模板中我们使用了 once
修饰符,但它并不会出现在 modifiers
对象中,其最终生成的元素描述对象如下:
ast = {
events: {
'~click': {
value: 'handleClick',
modifiers: {}
}
}
}
可以看到 modifiers
是一个空对象,但是事件名字由 click
变成了 ~click
。实际上对于一个使用了 once
修饰符的事件绑定,解析器会在原始事件名称前添加 ~
符并将其作为新的事件名称,接着会忽略 once
修饰符,所以 once
修饰符不会出现在 modifiers
对象中。为什么要忽略 once
修饰符呢?因为对于后面的程序来讲,该修饰符已经没有使用的必要的,因为通过检查事件名称的第一个字符是否为 ~
即可判断该事件是否为 once
的。除了 once
修饰符之外,以下列出的修饰符也不会出现在 modifiers
对象中:
click
并使用了 right
修饰符,则 right
修饰符不会出现在 modifiers
对象中,因为在解析阶段使用了 right
修饰符的 click
事件会被重写为 contextmenu
事件,假如有如下模板:<div @click.right="handler"></div>
其元素描述对象为:
ast = {
events: {
contextmenu: {
value: "handler",
modifiers: {}
}
}
}
capture
、passive
修饰符不会出现在 modifiers
对象中,原因与 once
修饰符一样,capture
、passive
修饰符也会修改事件的名称,其中 capture
修饰符会在原始事件名称之前添加 !
,passive
修饰符会在事件名称之前添加 &
,假如有如下模板<div @click.capture="handler"></div>
<div @click.passive="handler"></div>
则对于的元素描述对象分别为:
// 使用了 `capture` 修饰符
ast = {
events: {
'!click': {
value: "handler",
modifiers: {}
}
}
}
// 使用了 `passive` 修饰符
ast = {
events: {
'&click': {
value: "handler",
modifiers: {}
}
}
}
native
修饰符也不会出现在 modifiers
对象中,原因很简单,native
修饰符是用来给解析器使用的,当解析器遇到使用了 native
修饰符的事件,则会将事件信息添加到元素描述对象的 nativeEvents
属性中,而不是 events
属性中,例如:<comp @click.native="handler"></copm>
则其元素描述对象为:
ast = {
nativeEvents: {
click: {
value: "handler",
modifiers: {}
}
}
}
除了以上修饰符之外,其他所有修饰符都会出现在 modifiers
对象中。
节点元素对象的 directives
属性是一个数组,用来保存标签中所有指令信息。但并不是所有指令信息都会保存在 directives
数组中,比如 v-for
指令和 v-if
指令等等,因为这些指令在之前的处理中已经被移除掉。总的来说,指令分为内置指令和自定义指令,真正会出现在 directives
数组中的只有部分内置指令以及全部自定义指令。
不会出现在 directives
数组中的内置指令有:v-pre
、v-for
、v-if
、v-else-if
、v-else
以及 v-once
。
会出现在 directives
数组中的内置有:v-text
、v-html
、v-show
、v-model
以及 v-cloak
。
另外 v-on
、v-bind
是两个比较特殊的指令,当这两个指令拥有参数时,则不会出现在 directives
数组中,比如:
<div v-on:click="handler"></div>
<div v-bind:some-prop="val"></div>
以上这两中写法,由于 v-on
和 v-bind
指令拥有参数,所以这两个指令不会出现在 directives
,但是我们知道 v-on
和 v-bind
指令可以直接绑定对象,此时他们是没有参数的:
<div v-on="$listeners"></div>
<div v-bind="$attrs"></div>
这时候 v-on
和 v-bind
指令都会出现在 directives
数组中。为什么同样指令不同的使用方式会得到不同的对待呢?其实正是由于使用方式的不同,才需要不同的处理,在代码生成阶段,我们会更加理解这一点。
一个完整的指令由四部分组成,分别是:指令的名称
、指令表达式
、指令参数
以及 指令修饰符
,假设有如下模板:
<div v-custom-dir:arg.modif="val"></div>
如上模板展示了一个完整的指令,最终其生成的元素描述对象为:
ast = {
directives: [
{
name: 'custom-dir',
rawName: 'v-custom-dir:arg.modif',
value: 'val',
arg: 'arg',
modifiers: {
modif: true
}
}
]
}
如果以标签使用了静态 class
,即非绑定的 class
,那么该标签的元素描述对象将拥有 staticClass
属性,假设有如下模板:
<div class="a b c"></div>
则其元素描述对象为:
ast = {
staticClass: '"a b c"'
}
staticClass
属性中存储的是静态 class
,而元素描述对象的 classBinding
属性中所存储的则是绑定的 class
,假设有如下模板:
<div :class="{ active: true }"></div>
则其元素描述对象为:
ast = {
classBinding: '{ active: true }'
}
节点元素描述对象的 staticStyle
属于包含的是静态 style
内联样式信息,假设有如下模板:
<div style="color: red; background: green;"></div>
则其元素描述对象为:
ast = {
staticStyle: '{"color":"red","background":"green"}'
}
可以发现 staticStyle
属性的值不是简单的把 style
内联样式拷贝下来,而是将其解析成了对象的样子。
styleBinding
属性类似于 classBinding
属性。假设有如下模板:
<div :style="{ backgroundColor: green }"></div>
则其元素描述对象为:
ast = {
styleBinding: '{ backgroundColor: green }'
}
节点元素描述对象的 plain
属性是一个布尔值,plain
属性的真假将影响代码生成阶段对于 VNodeData
的生成。什么是 VNodeData
呢?在 Vue
中一个 VNode
代表一个虚拟节点,而 VNodeData
就是用来描述该虚拟节点的管家信息。在代码生成节点我们会发现 AST
中元素的大部分信息都用来生成 VNodeData
。对于一个节点的元素描述对象来讲,如果其 plain
属性值为 true
,该节点所对应的虚拟节点将不包含任何 VNodeData
。
1、如果一个标签是使用了 v-pre
指令标签的子代标签,则该标签元素描述对象的 plain
属性将使用为 true
。但要注意的是,使用了 v-pre
指令的那个标签的元素描述对象的 plain
属性不为 true
。
2、如果你标签既没有使用特性 key
,又没有任何属性,那么该标签的元素描述对象的 plain
属性将始终为 true
。
其实,我们完全可以认为,只有使用了 v-pre
指令的标签的子待节点其元素描述对象的 plain
属性才会为 true
。
节点元素描述对象的 isComment
属性是一个布尔值,用来标识当前节点是否是注释节点。所以只有注释节点的元素描述对象才会有这个属性,并且其值为 true
。
我们通过调用$.Callbacks获取到一个 callback 实例,如下
var cb = $.Callbacks();
看到Callbacks首字母大写,有些人可能会觉得一般只有对象才会这样,因此需要 new 一个实例,如下
var cb = new $.Callbacks();
实际上这两种方式都可以,因为 Callbacks 函数返回值是一个对象,为什么会这样?看下面一组对比
function Cons() {
this.name = 'this.name';
return {
name: 'obj.name'
};
}
console.log(Cons().name);//obj.name
console.log(new Cons().name);//obj.name
function Cons() {
this.name = 'this.name';
return 'str.name';
}
console.log(Cons());//str.name
console.log(new Cons().name);//this.name
当函数的返回值是一个对象时(null 除外),new 和直接调用两者的返回值是一样的。但是需要注意了,两者的 this 指向是不一样的。为了尽可能的节省代码和避免混乱我们还是统一采用 var cb = $.Callbacks(); 的方式去调用。
像这种先调用获取到实例,然后通过实例进行一系列的操作,很明显利用了闭包特性。
向内部队列添加函数,总有三种参数形式
var cb = $.Callbacks();
cb.add(function () {
console.log('add one');
});
var cb = $.Callbacks();
cb.add(function () {
console.log('add one');
}, function () {
console.log('add two');
});
var cb = $.Callbacks();
cb.add([
function () {
console.log('add one');
}, function () {
console.log('add two');
}
]);
依次执行队列里的函数
var cb = $.Callbacks();
cb.add([
function () {
console.log('add one');
}, function () {
console.log('add two');
}
]);
cb.fire();
//add one
//add two
fire的参数会传递给我们添加的函数,例如
var cb = $.Callbacks();
cb.add(function (name, age) {
console.log(name, age);
});
cb.fire('Jacky', 26);//Jacky 26
fire 调用的时候,我们添加函数当中的 this 指向我们的 Callbacks 实例,例如
var cb = $.Callbacks();
cb.add(function () {
console.log(this === cb);
});
cb.fire();//true
fireWith就是改变我们添加函数的context,即this指向,例如
var cb = $.Callbacks();
var obj = {
name: 'objName'
};
cb.add(function (age) {
console.log(this.name, age);
});
cb.fireWith(obj, [26]);//objName 26
fireWith第一个参数是我们的context,第二个参数是我们需要传递的内容数组,注意了是数组!
清空函数队列
var cb = $.Callbacks();
cb.add(function () {
console.log('add one');
});
cb.empty();
cb.fire();
var cb = $.Callbacks();
function demo() {
console.log('demo');
}
cb.add(demo);
console.log(cb.has(demo));//true
函数传递的都是引用,千万别出现以下的低级错误
var cb = $.Callbacks();
cb.add(function () {
console.log('demo');
});
cb.has(function () {
console.log('demo');
});
从内部队列中移除某些函数
var cb = $.Callbacks();
function demo1() {
console.log('demo1');
}
function demo2() {
console.log('demo2');
}
cb.add(demo1, demo2);
cb.remove(demo1, demo2);
cb.fire();
禁用回调列表。这种情况会清空函数队列,禁用核心功能。意味着这个回调管理报废了。
var cb = $.Callbacks();
cb.add(function () {
console.log('add');
});
cb.disable();
cb.fire();
回调管理是否被禁用
var cb = $.Callbacks();
cb.add(function () {
console.log('add');
});
cb.disable();
console.log(cb.disabled());//true
锁定回调管理,同disable,唯一的差别会在下面表述
回调管理是否被锁
回调队列是否执行过
var cb = $.Callbacks();
cb.add(function () {
console.log('add');
});
cb.fire();//add
console.log(cb.fired());//true
$.Callbacks通过字符串参数的形式支持4种及以上的特定功能。很明显的一个工厂模式。
函数队列只执行一次。参考以下对比
//不添加参数
var cb = $.Callbacks();
cb.add(function () {
console.log('add');
});
cb.fire();//add
cb.fire();//add
//添加参数
var cb = $.Callbacks('once');
cb.add(function () {
console.log('add');
});
cb.fire();//add
cb.fire();
函数队列执行过以后,就不会在执行了,无论调用fire多少次。
往内部队列添加的函数保持唯一,不能重复添加。参考以下对比
//不添加参数
var cb = $.Callbacks();
function demo() {
console.log('demo');
}
cb.add(demo, demo);
cb.fire();
//demo
//demo
//添加参数
var cb = $.Callbacks('unique');
function demo() {
console.log('demo');
}
cb.add(demo, demo);
cb.fire();//demo
相同的函数不会被重复添加到内部队列中
内部队列里的函数是依次执行的,当某个函数的返回值是false时,停止继续执行剩下的函数。参考以下对比
//不添加参数
var cb = $.Callbacks();
cb.add([
function () {
console.log('add one');
},
function () {
console.log('add two');
}
]);
cb.fire();
//add one
//add two
//添加参数
var cb = $.Callbacks('stopOnFalse');
cb.add([
function () {
console.log('add one');
return false;
},
function () {
console.log('add two');
}
]);
cb.fire();//add one
注意了返回值一定要是false,像undefined、null这种作为返回值是没有效果的
当函数队列fire或fireWith一次过后,内部会记录当前fire或fireWith的参数。当下次调用add的时候,会把记录的参数传递给新添加的函数并立即执行这个新添加的函数。看个例子
var cb = $.Callbacks('memory');
cb.add(function (name) {
console.log('one', name);
});
cb.fire('Jacky');//first Jacky
cb.add(function (name) {
console.log('two', name);
});//two Jacky
例如公司领导在9点的时候发了封邮件,要求大家提交自己的年终终结,这就相当于 fire 操作了,在公司里的员工收到邮件后,立马提交了。小李由于请假,下午才过来,看到邮件后也提交了自己的年终总结。不需要领导再次发送邮件提醒。
fire 或 fireWith 一定要在 disabled 或 lock 前先执行一遍,memory 才会起作用
这四种基本类型可以相互组合起来使用,例如 $.Deferred 就使用了 once 和 memory 的组合。
jQuery.Callbacks("once memory")
两者唯一的区别就是添加了memory参数,看一下对比
var cb = $.Callbacks('memory');
cb.add(function () {
console.log('one');
});
cb.fire();
cb.disable();
//cb.lock();
cb.add(function () {
console.log('two');
});
毫无疑问,disable 就是禁用所有功能,无论添加什么参数。而在 memory 的情况下,fire 过后在 lock,继续 add 新的函数依旧会立即执行。
相信大家都有看过这样的一道 javascript 的笔试题,具体如下:
var length = 10;
function fn () {
console.log(this.length);
}
var obj = {
length: 5,
method: function (fn) {
fn();
arguments[0]();
}
}
obj.method(fn, 1);
这道题主要考察的是 this 的指向和 arguments 对象,第二个输出为 2 ,这里我们不讨论这个话题。第一个输出的如果对 this 理解模糊的话容易答出 5 的答案,其实答案为 10 。
分析:javascript 判断 this 绑定的判定规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,先思考如下代码:
function fn () {
console.log(this.a);
}
var obj = {
a: 2,
fn: fn
}
obj.fn(); //2
首先需要注意的是 fn() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。但是无论是直接在 obj 定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象。然而,调用位置会使用 obj 的上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥有”或者“包含”它。
无论你如何称呼这个模式,但 fn() 被调用时,它的前面确实加上了对 obj 的引用。当函数引用有上下文对象时,函数调用中的 this 会绑定到这个上下文对象。因此调用 fn() 时 this 被绑定到 obj ,因此 this.a 和 obj.a 时一样的。
一种最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上。
思考下面的代码:
function fn () {
console.log(this.a);
}
var obj = {
a: 2,
fn: fn
}
var bar = obj.fn; //函数别名
bar();//undefined
虽然 bar 是 obj.fn 的一个引用,但是实际上,它引用的是 fn 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此为 undefined 。
回到之前的笔试题,参数传递其实就是一种隐式的赋值,下面的例子的参数传递和上面例子的 var bar = obj.fn 其实是一样的。所以答案为 10 。
var length = 10;
function fn () {
console.log(this.length);
}
var obj = {
length: 5,
method: function (fn) {//fn其实引用的是function fn()
fn();//这里才是真正的调用位置
arguments[0]();
}
}
obj.method(fn,1);
那如果把函数传入到语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没有区别:
function fn () {
console.log(this.a);
}
var obj = {
a: 2,
fn: fn
}
var a = "global";
setTimeout(obj.fn,100);
javascript 环境中内置的 setTimeout() 函数的实现和下面的伪代码类似:
function setTimeout (fn,delay) {
//等待delay毫秒
fn();//调用位置
}
就像我们看到的那样,回调函数丢失 this 绑定是很常见的。除此之外,还有一种情况 this 的行为出乎我们的意料:调用回调函数的函数可能会修改 this 。在一些流行的 javascript 框架中的事件处理器常会把回调函数的 this 强制绑定到触发事件的 dom 元素上。最后来一道测试题:
function fn () {
console.log(this.a);
}
var a = 2;
var o = { a:3,fn:fn};
var p = { a:4}
o.fn();//输出?
(p.fn = o.fn)();//输出?
答案:3和2。
最后,分享下分析这种隐式绑定的原则:如果在一个对象内包含一个指向函数的属性,并通过这个属性间接引用函数,只有在这种情况下的 this 才会绑定到这个对象上。
同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。
一个源的定义:如果协议,端口(如果指定了一个)和主机对于两个页面是相同的,则两个页面具有相同的源。
下表给出了相对 http://store.company.com/dir/page.html 同源检测的示例:
URL | 结果 | 原因 |
---|---|---|
http://store.company.com/dir/inner/another.html | 成功 | 同一域名 |
http://store.company.com/dir2/other.html | 成功 | 同一域名下不同文件夹 |
https://store.company.com/secure.html | 失败 | 不同的协议 ( https ) |
http://store.company.com:81/dir/etc.html | 失败 | 不同的端口 ( 81 ) |
http://news.company.com/dir/other.html | 失败 | 不同的主机 ( news ) |
document.domain的场景只适用于不同子域的框架间的交互,及主域必须相同的不同源。
页面可能会更改其自己的来源,但有一些限制。脚本可以将 document.domain的值设置为其当前域或其当前域的超级域。如果将其设置为其当前域的超级域,则较短的域将用于后续原始检查。例如,假设文档中的一个脚本在 http://store.company.com/dir/other.html 执行以下语句:
document.domain = "company.com";
这条语句执行之后,页面将会成功地通过对 http://company.com/dir/page.html 的同源检测。
注:浏览器单独保存端口号。任何的赋值操作,包括document.domain = document.domain都会以null值覆盖掉原来的端口号。因此,company.com:8080页面的脚本不能仅通过设置document.domain = "company.com"就能与company.com通信。赋值时必须带上端口号,以确保端口号不会为null。
(1) 在www.a.com/a.html中:
document.domain = 'a.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://www.script.a.com/b.html';
ifr.display = none;
document.body.appendChild(ifr);
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
ifr.onload = null;
};
(2) 在www.script.a.com/b.html中:
document.domain = 'a.com';//注意:使用document.domain允许子域安全访问其父域时,您需要设置document域在父域和子域中具有相同的值。这是必要的,即使这样做只是将父域设置回其原始值。否则可能会导致权限错误。这里都是a.com。
假设域名a.com下的文件cs1.html要和jianshu.com域名下的cs2.html传递信息。
1、cs1.html首先创建自动创建一个隐藏的iframe,iframe的src指向jianshu.com域名下的cs2.html页面。
2、cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据。
3、同时在cs1.html上加一个定时器,隔一段时间来判断location.hash的值有没有变化,一旦有变化则获取获取hash值。
注:由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe。
优点:1.可以解决域名完全不同的跨域。2.可以实现双向通讯。
缺点:location.hash会直接暴露在URL里,并且在一些浏览器里会产生历史记录,数据安全性不高也影响用户体验。另外由于URL大小的限制,支持传递的数据量也不大。有些浏览器不支持onhashchange事件,需要轮询来获知URL的变化。
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的。window.name属性的神奇之处在于name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。
window.name = data;//父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入。
location = 'http://parent.url.com/xxx.html';//接着,子窗口跳回一个与主窗口同域的网址。
var data = document.getElementById('myFrame').contentWindow.name。//然后,主窗口就可以读取子窗口的window.name了。
如果是与iframe通信的场景就需要把iframe的src设置成当前域的一个页面地址。
HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。postMessage的兼容性如下:
可以看到 Internet Explorer 8+, chrome,Firefox , Opera 和 Safari 都将支持这个功能。但是Internet Explorer 8和9以及Firefox 6.0和更低版本仅支持字符串作为postMessage的消息。
var popup = window.open('http://bbb.com', 'title');//父窗口http://aaa.com向子窗口http://bbb.com发消息,调用postMessage方法。
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。
父窗口和子窗口都可以通过message事件,监听对方的消息。message事件的事件对象event,提供以下三个属性:
一个例子:
var onmessage = function (event) {
var data = event.data;//消息
var origin = event.origin;//消息来源地址
var source = event.source;//源Window对象
if(origin == "http://www.aaa.com"){
console.log(data);//hello world!
}
source.postMessage('Nice to see you!', '*');
};
if (typeof window.addEventListener != 'undefined') {
window.addEventListener('message', onmessage, false);
} else if (typeof window.attachEvent != 'undefined') {
//ie
window.attachEvent('onmessage', onmessage);
}
基本原理:网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。例子如下:
function todo(data){
console.log('The author is: '+ data.name);
}
var script = document.createElement('script');
script.src = 'http://www.jianshu.com/author?callback=todo';//向服务器www.jianshu.com发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字。
document.body.appendChild(script);
//服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
todo({"name": "fewjq"});
//由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了todo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象。
优点:简单适用,老式浏览器全部支持,服务器改造小。不需要XMLHttpRequest或ActiveX的支持。
缺点:只支持GET请求。
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
当一个资源请求一个其它域名的资源时会发起一个跨域HTTP请求(cross-origin HTTP request)。比如说,域名A的某 Web 应用通过标签引入了域名B的某图片资源,域名A的 Web 应用就会导致浏览器发起一个跨域 HTTP 请求。在当今的 Web 开发中,使用跨域 HTTP 请求加载各类资源(包括CSS、图片、JavaScript 脚本以及其它类资源),已经成为了一种普遍且流行的方式。
正如大家所知,出于安全考虑,浏览器会拦截跨域请求的返回结果。比如,使用XMLHttpRequest对象和Fetch发起 HTTP 请求就必须遵守“同源策略”。 具体而言,Web 应用程序通过XMLHttpRequest对象或Fetch能且只能向同域名的资源发起 HTTP 请求,而不能向任何其它域名发起请求。为了能开发出更强大、更丰富、更安全的Web应用程序,开发人员渴望着在不丢失安全的前提下,Web 应用技术能越来越强大、越来越丰富。比如,可以使用 XMLHttpRequest 发起跨站 HTTP 请求。
隶属于 W3C 的 Web 应用工作组推荐了一种新的机制,即跨源资源共享(Cross-Origin Resource Sharing ) CORS。这种机制让Web应用服务器能支持跨站访问控制,从而使得安全地进行跨站数据传输成为可能。需要特别注意的是,这个规范是针对API容器的(比如说XMLHttpReques 或者 Fetch),以减轻跨域HTTP请求的风险。
跨源资源共享标准( cross-origin sharing standard) 使得以下场景可以使用跨站 HTTP 请求:
所有浏览器都支持该功能,IE浏览器不能低于IE10。通过 XMLHttpRequest 对象发起。但Internet Explorer 8 和 9 可以通过 XDomainRequest 对象来实现CORS。可以把CORS分为:简单请求、预请求和附带凭证信息的请求。
所谓的简单,是指:
(1)只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种。
(2)不会使用自定义请求头(类似于 X-Modified 这种)。
一个例子:
//比如说,假如站点 http://foo.example 的网页应用想要访问 http://bar.other 的资源。以下的 JavaScript 代
//码应该会在 foo.example 上执行:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
function callOtherDomain() {
if(invocation) {
invocation.open('GET', url, true);
invocation.onreadystatechange = handler;
invocation.send();
}
}
//让我们看看,在这个场景中,浏览器会发送什么的请求到服务器,而服务器又会返回什么给浏览器:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130
Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example //该请求来自于 http://foo.exmaple。
//以上是浏览器发送请求
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: * //这表明服务器接受来自任何站点的跨站请求。如果设置为http://foo.example。其它站点就不能跨站访问 http://bar.other 的资源了。
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
//以上是服务器返回信息给浏览器
如上,通过使用 Origin 和 Access-Control-Allow-Origin 就可以完成最简单的跨站请求。不过服务器需要把 Access-Control-Allow-Origin 设置为 * 或者包含由 Origin 指明的站点。
不同于上面讨论的简单请求,“预请求”要求必须先发送一个 OPTIONS 请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。这样做,是因为跨站请求可能会对目的站点的数据造成破坏。 当请求具备以下条件,就会被当成预请求处理:
(1)请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 或者 text/xml 的 XML 数据的请求。
(2)使用自定义请求头(比如添加诸如 X-PINGOTHER)
一个例子:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}Arun';
function callOtherDomain(){
if(invocation){
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
如上,以 XMLHttpRequest 创建了一个 POST 请求,为该请求添加了一个自定义请求头(X-PINGOTHER: pingpong),并指定数据类型为 application/xml。所以,该请求是一个“预请求”形式的跨站请求。浏览器使用一个 OPTIONS 发送了一个“预请求”。Firefox 3.1 根据请求参数,决定需要发送一个“预请求”,来探明服务器端是否接受后续真正的请求。 OPTIONS 是 HTTP/1.1 里的方法,用来获取更多服务器端的信息,是一个不应该对服务器数据造成影响的方法。 随同 OPTIONS 请求,以下两个请求头一起被发送:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER
假设服务器成功响应返回部分信息如下:
Access-Control-Allow-Origin: http://foo.example //表明服务器允许http://foo.example的请求
Access-Control-Allow-Methods: POST, GET, OPTIONS //表明服务器可以接受POST, GET和 OPTIONS的请求方法
Access-Control-Allow-Headers: X-PINGOTHER //传递一个可接受的自定义请求头列表。服务器也需要设置一个与浏览器对应。否则会报 Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers in preflight response 的错误
Access-Control-Max-Age: 1728000 //告诉浏览器,本次“预请求”的响应结果有效时间是多久。在上面的例子里,1728000秒代表着20天内,浏览器在处理针对该服务器的跨站请求,都可以无需再发送“预请求”,只需根据本次结果进行判断处理。
XMLHttpRequest 和访问控制功能,最有趣的特性就是,发送凭证请求(HTTP Cookies和验证信息)的功能。一般而言,对于跨站请求,浏览器是不会发送凭证信息的。但如果将 XMLHttpRequest 的一个特殊标志位设置为true,浏览器就将允许该请求的发送。
一个例子:
//http://foo.example站点的脚本向http://bar.other站点发送一个GET请求,并设置了一个Cookies值。脚本代码如下:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain(){
if(invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
如上,第七行代码将 XMLHttpRequest 的withCredentials标志设置为true,从而使得Cookies可以随着请求发送。因为这是一个简单的GET请求,所以浏览器不会发送一个“预请求”。但是,如果服务器端的响应中,如果没有返回Access-Control-Allow-Credentials: true的响应头,那么浏览器将不会把响应结果传递给发出请求的脚本程序,以保证信息的安全。
假设服务器成功响应返回部分信息如下:
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
如果bar.other的响应头里没有Access-Control-Allow-Credentials:true,则响应会被忽略.。特别注意: 给一个带有withCredentials的请求发送响应的时候,服务器端必须指定允许请求的域名,不能使用“*”。上面这个例子中,如果响应头是这样的 Access-Control-Allow-Origin:* ,则响应会失败。在这个例子里,因为Access-Control-Allow-Origin的值是 http://foo.example 这个指定的请求域名,所以客户端把带有凭证信息的内容被返回给了客户端。另外注意,更多的cookie信息也被创建了。
CORS的优点:CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案。
参考原文:https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/
在本文中,我们将探讨客户端JavaScript代码中常见的内存泄漏类型。 我们还将学习如何使用Chrome开发工具找到它们。
内存泄漏是每个开发人员都要面临的问题。 即使使用内存管理的语言,也存在内存泄漏的情况。 内存泄漏是导致迟缓,崩溃,高延迟的根本原因,甚至会导致其他应用问题。
实质上,内存泄漏可以定义为应用程序不再需要的内存,因为某种原因其不会返回到操作系统或可用内存池。编程语言有不同的管理内存的方式。这些方法可以减少泄漏内存的机会。然而,某一块内存是否未被使用实际上是一个不可判定的问题。 换句话说,只有开发人员才能明确是否可以将一块内存返回到操作系统。 某些编程语言提供了帮助开发人员执行此操作的功能。
JavaScript是垃圾回收语言之一。 垃圾回收语言通过定期检查哪些先前分配的内存是否“可达”来帮助开发人员管理内存。 换句话说,垃圾回收语言将管理内存的问题从“什么内存仍可用? 到“什么内存仍可达?”。区别是微妙的,但重要的是:虽然只有开发人员知道将来是否需要一块分配的内存,但是不可达的内存可以通过算法确定并标记为返回到操作系统。
非垃圾回收的语言通常使用其他技术来管理内存:显式管理,开发人员明确告诉编译器何时不需要一块内存; 和引用计数,其中使用计数与存储器的每个块相关联(当计数达到零时,其被返回到OS)。
垃圾回收语言泄漏的主要原因是不需要的引用。要理解什么不需要的引用,首先我们需要了解垃圾回收器如何确定一块内存是否“可达”。
垃圾回收语言泄漏的主要原因是不需要的引用。
Mark-and-sweep
大多数垃圾回收器使用称为标记和扫描的算法。该算法由以下步骤组成:
现代垃圾回收器以不同的方式改进了该算法,但本质是相同的:可访问的内存段被标记,其余被垃圾回收。不需要的引用是开发者知道它不再需要,但由于某种原因,保存在活动根的树内部的内存段的引用。 在JavaScript的上下文中,不需要的引用是保存在代码中某处的变量,它不再被使用,并指向可以被释放的一块内存。 有些人会认为这些都是开发者的错误。所以要了解哪些是JavaScript中最常见的漏洞,我们需要知道在哪些方式引用通常被忽略。
JavaScript背后的目标之一是开发一种看起来像Java的语言,容易被初学者使用。 JavaScript允许的方式之一是处理未声明的变量:对未声明的变量的引用在全局对象内创建一个新的变量。 在浏览器的情况下,全局对象是窗口。 换一种说法:
function foo(arg) {
bar = "this is a hidden global variable";
}
事实上:
function foo(arg) {
window.bar = "this is an explicit global variable";
}
如果bar应该只在foo函数的范围内保存对变量的引用,并且您忘记使用var来声明它,那么会创建一个意外的全局变量。 在这个例子中,泄漏一个简单的字符串可能没什么,但有更糟糕的情况。
创建偶然的全局变量的另一种方式是通过下面这样:
function foo() {
this.variable = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();
为了防止这些错误发生,添加'use strict'; 在您的JavaScript文件的开头。 这使得能够更严格地解析JavaScript以防止意外的全局变量。
即使我们讨论了不可预测的全局变量,但是仍有一些明确的全局变量产生的垃圾。这些是根据定义不可回收的(除非被取消或重新分配)。特别地,用于临时存储和处理大量信息的全局变量是令人关注的。 如果必须使用全局变量来存储大量数据,请确保将其置空或在完成后重新分配它。与全局变量有关的增加的内存消耗的一个常见原因是高速缓存)。缓存存储重复使用的数据。 为了有效率,高速缓存必须具有其大小的上限。 无限增长的缓存可能会导致高内存消耗,因为缓存内容无法被回收。
setInterval的使用在JavaScript中是很常见的。大多数这些库在它们自己的实例变得不可达之后,使得对回调的任何引用不可达。在setInterval的情况下,但是,像这样的代码是很常见的:
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// Do stuff with node and someResource.
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
此示例说明了挂起计时器可能发生的情况:引用不再需要的节点或数据的计时器。 由节点表示的对象可以在将来被移除,使得区间处理器内部的整个块不需要了。 但是,处理程序(因为时间间隔仍处于活动状态)无法回收(需要停止时间间隔才能发生)。 如果无法回收间隔处理程序,则也无法回收其依赖项。 这意味着someResource,它可能存储大小的数据,也不能被回收。
对于观察者的情况,重要的是进行显式调用,以便在不再需要它们时删除它们(或者相关对象即将无法访问)。 在过去,以前特别重要,因为某些浏览器(Internet Explorer 6)不能管理循环引用(参见下面的更多信息)。 现在,一旦观察到的对象变得不可达,即使没有明确删除监听器,大多数浏览器也可以回收观察者处理程序。 然而,在对象被处理之前显式地删除这些观察者仍然是良好的做法。 例如:
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
关于对象观察者和循环引用:
观察者和循环引用曾经是JavaScript开发者的祸根。 这是由于Internet Explorer的垃圾回收器中的错误(或设计决策)。旧版本的Internet Explorer无法检测DOM节点和JavaScript代码之间的循环引用。这是一个典型的观察者,通常保持对可观察者的引用(如上例所示)。换句话说,每当观察者被添加到Internet Explorer中的一个节点时,它就会导致泄漏。这是开发人员在节点或在观察者中引用之前明确删除处理程序的原因。 现在,现代浏览器(包括Internet Explorer和Microsoft Edge)使用现代垃圾回收算法,可以检测这些周期并正确处理它们。 换句话说,在使节点不可达之前,不必严格地调用removeEventListener。框架和库(jQuery)在处理节点之前删除侦听器(当为其使用特定的API时)。这是由库内部处理,并确保不产生泄漏,即使运行在有问题的浏览器,如旧的Internet Explorer。
有时,将DOM节点存储在数据结构中可能很有用。 假设要快速更新表中多行的内容。 在字典或数组中存储对每个DOM行的引用可能是有意义的。 当发生这种情况时,会保留对同一个DOM元素的两个引用:一个在DOM树中,另一个在字典中。 如果在将来的某个时候,您决定删除这些行,则需要使这两个引用不可访问。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
}
function removeButton() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}
对此的另外考虑与对DOM树内的内部或叶节点的引用有关。 假设您在JavaScript代码中保留对表的特定单元格(标记)的引用。 在将来的某个时候,您决定从DOM中删除表,但保留对该单元格的引用。 直观地,可以假设GC将回收除了该单元之外的所有东西。 在实践中,这不会发生:单元格是该表的子节点,并且子级保持对其父级的引用。 换句话说,从JavaScript代码对表单元格的引用导致整个表保留在内存中。 在保持对DOM元素的引用时仔细考虑这一点。
JavaScript开发的一个关键方面是闭包:从父作用域捕获变量的匿名函数。 Meteor开发人员发现了一个特定的情况,由于JavaScript运行时的实现细节,可能以一种微妙的方式泄漏内存:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
这个片段做了一件事:每次replaceThing被调用,theThing获取一个新的对象,其中包含一个大数组和一个新的闭包(someMethod)。同时,unused变量保持一个闭包,该闭包具有对originalThing的引用(来自之前对replaceThing的调用的Thing)。已经有点混乱了,是吗?重要的是,一旦为同一父作用域中的闭包创建了作用域,则该作用域是共享的。在这种情况下,为闭包someMethod创建的作用域由unused共享。unused的引用了originalThing。即使unused未使用,可以通过theThing使用someMethod。由于someMethod与unused共享闭包范围,即使未使用,它对originalThing的引用强制它保持活动(防止其收集)。当此代码段重复运行时,可以观察到内存使用量的稳定增加。这在GC运行时不会变小。实质上,创建一个闭包的链接列表(其根以theThing变量的形式),并且这些闭包的范围中的每一个都包含对大数组的间接引用,导致相当大的泄漏。
Meteor的博文解释了如何修复此种问题。在replaceThing的最后添加originalThing = null。
垃圾回收器的不直观行为:
虽然垃圾回收器很方便,但他们有自己的一套权衡。 这些权衡之一是非确定性。 换句话说,GC是不可预测的。 通常不可能确定何时执行回收。 这意味着在某些情况下,正在使用比程序实际需要的更多的内存。 在其他情况下,短暂停顿在特别敏感的应用中可能是明显的。 虽然非确定性意味着无法确定何时执行集合,但大多数GC实现都分享在分配期间执行集合传递的常见模式。 如果没有执行分配,则大多数GC保持静止。 考虑以下情况:
在这种情况下,大多数GC不会运行任何进一步的集合过程。 换句话说,即使有不可达的引用可用于回收,回收器也不会回收这些引用。 这些不是严格的泄漏,但仍然导致高于通常的内存使用。
Google在他们的JavaScript内存分析文档中提供了这种行为的一个很好的例子,next!!!。
Chrome提供了一组很好的工具来分析JavaScript代码的内存使用情况。 有两个与内存相关的基本视图:时间轴视图和配置文件视图。
TimeLine对于在代码中发现异常内存模式至关重要。 如果我们正在寻找大的泄漏,周期性的跳跃,收缩后不会收缩,就像一个红旗。 在这个截图中,我们可以看到泄漏对象的稳定增长可能是什么样子。 即使在大收集结束后,使用的内存总量高于开始时。 节点计数也较高。 这些都是代码中某处泄露的DOM节点的迹象。
这是你将花费大部分时间看的视图。 Profiles允许您获取快照并比较JavaScript代码的内存使用快照。 它还允许您记录分配的时间。 在每个结果视图中,不同类型的列表都可用,但是对于我们的任务最相关的是summary(概要)列表和comparison(对照)列表。
summary(概要)列表为我们概述了分配的不同类型的对象及其聚合大小:浅大小(特定类型的所有对象的总和)和保留大小(浅大小加上由于此对象保留的其他对象的大小 )。 它还给了我们一个对象相对于它的GC根(距离)有多远的概念。
comparison(对照)给了我们相同的信息,但允许我们比较不同的快照。 这对于查找泄漏是非常有用的。
基本上有两种类型的泄漏:1、泄漏引起内存使用的周期性增加。2、一次发生的泄漏,并且不会进一步增加内存。
由于明显的原因,当它们是周期性的时更容易发现泄漏。这些也是最麻烦的:如果内存在时间上增加,这种类型的泄漏将最终导致浏览器变慢或停止脚本的执行。不是周期性的泄漏可以很容易地发现。这通常会被忽视。在某种程度上,发生一次的小泄漏可以被认为是优化问题。然而,周期性的泄漏是错误并且必须解决的。
对于我们的示例,我们将使用Chrome的文档中的一个示例。 完整代码粘贴如下:
var x = [];
function createSomeNodes() {
var div,
i = 100,
frag = document.createDocumentFragment();
for (;i > 0; i--) {
div = document.createElement("div");
div.appendChild(document.createTextNode(i + " - "+ new Date().toTimeString()));
frag.appendChild(div);
}
document.getElementById("nodes").appendChild(frag);
}
function grow() {
x.push(new Array(1000000).join('x'));
createSomeNodes();
setTimeout(grow,1000);
}
当调用grow时,它将开始创建div节点并将它们附加到DOM。它还将分配一个大数组,并将其附加到全局变量引用的数组。这将导致使用上述工具可以找到的内存的稳定增加。
Timeline非常有用。 在Chrome中打开示例,打开开发工具,转到Timeline,选择Memory,然后点击录制按钮。 然后转到页面并单击按钮开始泄漏内存。 一段时间后停止录制,看看结果:
此示例将继续每秒泄漏内存。停止录制后,在grow函数中设置断点,以停止脚本强制Chrome关闭页面。在这个图像有两个大的迹象,表明我们正在记录泄漏。节点(绿线)和JS堆(蓝线)的图。节点正在稳步增加,从不减少。这是一个大的警告标志。
JS堆也显示内存使用的稳定增长。这是很难看到由于垃圾回收器的影响。您可以看到初始内存增长的模式,随后是大幅下降,随后是增加,然后是尖峰,继续记忆的另一下降。 在这种情况下的关键在于事实,在每次内存使用后,堆的大小保持大于上一次下降。 换句话说,虽然垃圾收集器正在成功地收集大量的存储器,但是它还是周期性地泄漏了。
1、获取两个快照
要查找泄漏,我们现在将转到Chrome的开发工具的profiles部分。要将内存使用限制在可管理的级别,请在执行此步骤之前重新加载页面。我们将使用Take Heap Snapshot函数。
重新加载页面,并在完成加载后立即获取堆快照。 我们将使用此快照作为我们的基线。之后,再次点击最左边的Profiles按钮,等待几秒钟,并采取第二个快照。捕获快照后,建议在脚本中设置断点,以防止泄漏使用更多内存。
有两种方法可以查看两个快照之间的分配。 选择summary(摘要),右侧选择 Objects allocated between Snapshot 1 and Snapshot 2,或者筛选菜单选择 Comparison。在这两种情况下,我们将看到在两个快照之间分配的对象的列表。
在这种情况下,很容易找到泄漏:他们很大。看看 (string) 的 Size Delta Constructor,8MB,58个新对象。 这看起来很可疑:新对象被分配,但是没有释放,占用了8MB。
如果我们打开 (string) Constructor的分配列表,我们将注意到在许多小的分配之间有一些大的分配。大者立即引起我们的注意。如果我们选择其中的任何一个,我们可以在下面的retainers部分得到一些有趣的东西。
我们看到我们选择的分配是数组的一部分。反过来,数组由全局窗口对象内的变量x引用。这给了我们从我们的大对象到其不可收回的根(窗口)的完整路径 我们发现我们的潜在泄漏和被引用的地方。
到现在为止还挺好。但我们的例子很容易:大分配,例如在这个例子中的分配不是常态。幸运的是,我们的例子也泄漏了DOM节点,它们更小。使用上面的快照很容易找到这些节点,但在更大的网站,会变得更麻烦。 最新版本的Chrome提供了一个最适合我们工作的附加工具:记录堆分配功能。
2、Record heap allocations查找泄漏
禁用之前设置的断点,让脚本继续运行,然后返回Chrome的开发工具的“个人档案”部分。现在点击Record Heap Allocations。当工具运行时,您会注意到在顶部的图中的蓝色尖峰。这些代表分配。每秒大的分配由我们的代码执行。让它运行几秒钟,然后停止它(不要忘记再次设置断点,以防止Chrome吃更多的内存)。
在此图像中,您可以看到此工具的杀手锏:选择一段时间线以查看在该时间段内执行的分配。我们将选择设置为尽可能接近一个大峰值。列表中只显示了三个构造函数:其中一个是与我们的大漏洞((string))相关的构造函数,下一个与DOM分配相关,最后一个是Text构造函数(叶子DOM节点的构造函数 包含文本)。
#####从列表中选择一个 HTMLDivElement constructor,然后选择Allocation stack。
我们现在知道分配该元素的位置(grow - > createSomeNodes)。如果我们密切注意图中的每个尖峰,我们将注意到 HTMLDivElement constructor被调用了许多次。如果我们回到我们的快照比较视图,我们将注意到这个constructor显示许多分配,但没有删除。 换句话说,它正在稳定地分配内存,而没有被GC回收。从而我们知道这些对象被分配的确切位置(createSomeNodes函数)。现在回到代码,研究它,并修复漏洞。
3、另一个有用的功能
在堆分配结果视图中,我们可以选择Allocation视图。
这个视图给了一个与它们相关的函数和内存分配的列表。我们可以立即看到grow和createSomeNodes。当选择grow时,看看相关的object constructor。 可以注意到(string),HTMLDivElement和Text泄露了。
这些工具的组合可以大大有助于发现内存泄漏。在生产站点中执行不同的分析运行(理想情况下使用非最小化或模糊代码)。看看你是否能找到比他们应该保留更多的泄漏或对象(提示:这些更难找到)。
要使用此功能,请转到Dev Tools - >设置并启用“记录堆分配堆栈跟踪”。 在拍摄之前必须这样做。
内存泄漏可以并且确实发生在垃圾回收语言,如JavaScript。这些可以被忽视一段时间,最终他们将肆虐你的网站。因此,内存分析工具对于查找内存泄漏至关重要。分析运行应该是开发周期的一部分,特别是对于中型或大型应用程序。开始这样做,为您的用户提供最好的体验。
var Transform = require('readable-stream').Transform
, inherits = require('util').inherits
// 创建一个类
function DestroyableTransform(opts) {
// 调用父类构造函数
Transform.call(this, opts)
this._destroyed = false
}
// 继承 Transform 类的原型
inherits(DestroyableTransform, Transform)
// 添加 destroy 类方法
DestroyableTransform.prototype.destroy = function(err) {
if (this._destroyed) return
this._destroyed = true
var self = this
// 触发 destory 后,close 掉流
process.nextTick(function() {
if (err)
self.emit('error', err)
self.emit('close')
})
}
// a noop _transform function
// 一个空的 _transform 函数
function noop (chunk, enc, callback) {
callback(null, chunk)
}
// create a new export function, used by both the main export and
// the .ctor export, contains common logic for dealing with arguments
// 创建一个新的构造函数,用于主要的 through2.obj 和 through2.ctor
// 包含了处理 arguments 的逻辑
function through2 (construct) {
return function (options, transform, flush) {
// 实现第一个参数 options 可选
if (typeof options == 'function') {
flush = transform
transform = options
options = {}
}
// 如果 transform 不是一个函数,那么置 transform 为一个空的 _transform 函数(即不对流做任何处理)
if (typeof transform != 'function')
transform = noop
if (typeof flush != 'function')
flush = null
return construct(options, transform, flush)
}
}
// main export, just make me a transform stream!
// 主要的 export ,用于创建一个 transform 流
module.exports = through2(function (options, transform, flush) {
var t2 = new DestroyableTransform(options)
t2._transform = transform
if (flush)
t2._flush = flush
return t2
})
// make me a reusable prototype that I can `new`, or implicitly `new`
// with a constructor call
// 用于 new 使用,或者直接调用构建函数(隐式的的new)
module.exports.ctor = through2(function (options, transform, flush) {
// 创建一个新的构造函数
function Through2 (override) {
// 实现无 new 调用
if (!(this instanceof Through2))
return new Through2(override)
// 拓展 options
this.options = Object.assign({}, options, override)
// 继承 DestroyableTransform 的属性成员
DestroyableTransform.call(this, this.options)
}
// 继承 DestroyableTransform 的原型链
inherits(Through2, DestroyableTransform)
// 添加 _transform 函数
Through2.prototype._transform = transform
// 添加 _flush 函数
if (flush)
Through2.prototype._flush = flush
// 返回构造函数
return Through2
})
module.exports.obj = through2(function (options, transform, flush) {
// 由对象模式创建一个 transform 流
var t2 = new DestroyableTransform(Object.assign({ objectMode: true, highWaterMark: 16 }, options))
t2._transform = transform
if (flush)
t2._flush = flush
return t2
})
我们可以定义“数组子类”作为创建从原生数组对象(在其原型链中具有 Array.prototype)继承的对象的过程,并遵循与原生数组相似(或相同)的行为。
关于类似于原生数组的行为非常重要,我们后面会看到。 拥有数组的“子类”可以被认为能够创建一个数组对象,而不是直接从 Array 继承的对象,而是从另一个对象继承,然后才从 Array 继承。
换句话说,我们需要类似这样的行为:
var sub = new SubArray(1, 2, 3);
sub; // [1, 2, 3]
sub.length; // 3
sub[1]; // 2
sub.push(4);
sub; // [1, 2, 3, 4]
// 等等.
sub intanceof SubArray; // true
sub intanceof Array; // true
注意 SubArray 构造函数如何创建一个与数组行为相同的子对象(对象具有 “length” 属性,数字 “0”,“1”,“2” 属性,并继承 Array.prototype 上的方法)。 同时,SubArray 是直接继承的子对象,而不是 Array 。
那么做这一切的目的究竟是什么? 为什么以这种方式对数组进行继承?
通常有两个原因:
利用 Javascript 原型扩展数组对象方法很方便。 如下代码:
Array.prototype.last = function () {
return this[this.length - 1];
};
// ...
[1, 2, 3].last(); // 3
但是,扩展 Array.prototype 有代价的。当脚本与应用程序中的其他脚本共存时,这些脚本有可能相互冲突。扩展 Array.prototype 虽然诱人并且看起来很有用,但不幸的是在多样化的环境中不是很安全。不同的脚本可能最终定义相同名称的方法,但具有不同的行为。这种情况往往会导致不一致的行为和难以追踪的错误。
使用 Array 以外的构造函数 - 但具有相同的行为 - 可以避免这种冲突。不是扩展 Array.prototype,而是扩展另一个对象(比如 SubArray.prototype),然后用来初始化(子)数组对象。任何依赖 Array.prototype 方法的第三方代码仍然能够安全地使用它们。
继承数组的另一个原因是能够使用从数组继承的数据结构方法; 例如 Stack,List,Queue,Set (push,pop,shift,unshift 等)等方法。
我们可以使用原型式克隆方法:
function clone(obj) {
function F() { }
F.prototype = obj;
return new F();
}
然后设置如下的继承:
function Child() { }
Child.prototype = clone(Parent.prototype);
这里的原型链:
new Child()
|
| [[Prototype]]
|
v
Child.prototype
|
| [[Prototype]]
|
v
Parent.prototype
|
| [[Prototype]]
|
v
Object.prototype
|
| [[Prototype]]
|
v
null
开始实现继承数组:
function SubArray() {
// 将传递给构造函数的任何参数添加到实例中
this.push.apply(this, arguments);
}
SubArray.prototype = clone(Array.prototype);
var sub = new SubArray(1, 2, 3);
那么使用克隆方法继承数组究竟有什么错误? 让我们来看看之前声明的 SubArray 函数的行为。 我们将使用原生数组对象来进行比较。
var arr = new Array(1, 2, 3);
var sub = new SubArray(1, 2, 3);
arr.length; // 3
sub.length; // 0 (in IE<8)
arr.length = 2;
sub.length = 2;
arr; // [1, 2]
sub; // [1, 2, 3]
arr[10] = 'foo';
sub[10] = 'foo';
arr.length; // 11
sub.length; // 2
这里显然有些不一致。 即使我们忽略 IE < 8 中的错误。 但是,数组中的长度和数字属性之间的这种奇怪的关系是什么? 为什么不是和 Array 的行为相同? 为了理解这一点,我们需要查看 JavaScript 中的数组对象。
在 Javascript 中的数组几乎就像普通的 Object 对象,除了行为上的一点小差异。 如下引自 es 规范
Array objects give special treatment to a certain class of property names. A property name P (in the form of a string value) is an array index if and only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 2^32 - 1. Every Array object has a length property whose value is always a nonnegative integer less than 2^32. The value of the length property is numerically greater than the name of every property whose name is an array index; whenever a property of an Array object is created or changed, other properties are adjusted as necessary to maintain this invariant. Specifically, whenever a property is added whose name is an array index, the length property is changed, if necessary, to be one more than the numeric value of that array index; and whenever the length property is changed, every property whose name is an array index whose value is not smaller than the new length is automatically deleted. This constraint applies only to properties of the Array object itself and is unaffected by length or array index properties that may be inherited from its prototype.
可以概括为:数组对象以特殊的方式处理 “numeric” 属性。 只要这些属性发生变化,数组的 “length” 属性的值也会被调整; 它的调整是为了确保它总是比数组的最大索引大 1 。 类似地,当“长度”属性发生变化时,“numeric” 属性会相应地进行调整。
var arr = ['x', 'y', 'z'];
arr.length; // 3
arr = ['foo'];
arr.length; // 1
var arr = ['x', 'y'];
arr.length; // 2
arr[2] = 'z';
arr.length;
var arr = ['x', 'y', 'z'];
arr.length = 2;
arr; // ['x', 'y']
arr.length = 4;
arr; // ['x', 'y'] // “增加”长度不会影响数字属性...
arr.join(); // "x,y,," // 但在其他情况下可以看到后果,例如使用 `Array.prototype.push` 时
arr.push('z');
arr; // ['x', 'y', undefined, undefined, 'z']
现在你知道 Javascript 中的 Array 对象的“特殊”之处了,它处于 “length” 和 “numeric” 属性之间的关系中。 还有一个值得注意的细节是数组的 “length” 属性必须总是具有小于 2 ^ 32 的非负整数值。 只要违反这个条件,就会引发 RangeError 。
var arr = [];
arr.length = Math.pow(2, 32); // RangeError
arr.length; // 0 (长度仍然是0,就像它最初一样)
arr.length = Math.pow(2, 32) - 1; // 将长度设置为最大允许值
arr.length++; // RangeError (明确设置长度时)
arr.push(1); // RangeError (或者在隐式设置长度时)
为什么通过 SubArray 和 Array 函数创建的对象的行为存在差异。即使 SubArray 创建了一个从
Array.prototype 继承的对象,该对象完全没有数组的特殊行为。 SubArray 实例只不过是一个普通的
Object 对象(就像它是通过对象字面量创建的一样)。
但为什么 SubArray 创建一个 Object 对象而不是一个 Array 对象?这个问题的核心是 ECMAScript 中函数的工作方式。
当 new 运算符应用于对象时(如在新的 SubArray 中),调用该对象的内部 [[Constructor]] 方法。在我们的例子中,它是 SubArray 函数的 [[Constructor]] 。 SubArray - 作为本地函数 - 具有 [[Constructor]],它指定创建一个普通的 Object 对象,并调用提供新创建对象的相应函数作为此值。任何本地函数(包括SubArray)都应创建一个 Object 对象并将其作为结果返回。
现在值得一提的是,可以通过从构造函数显式返回对象来对 [[Constructor]] 的返回值进行排序:
function SubArray() {
this.push.apply(this, arguments);
return []; // 显式返回数组对象
}
但在这种情况下,返回的对象不会继承构造函数的“原型”(在这种情况下是 SubArray.prototype); 构造函数也不会被该对象调用。
var sub = new SubArray(1, 2, 3);
// 对象没有 1,2,3,因为构造函数从未被调用,返回的是 this 值引用
object
sub; // []
// SubArray 不在返回对象的原型链中
sub instanceof SubArray; // false
综上,创建一个从 Array.prototype 继承的对象只是开始。 最大的问题是保留长度和数字属性的特殊关系。 这就是为什么使用常规克隆方法不能完成的原因。
“为什么数组的特殊行为很重要”? 为什么当继承一个数组时,我们想要保持长度和数字属性之间的关系?
以 Array.prototype.push 为例, 要确定从哪个位置开始插入元素,push 将检索数组的 “length” 值。 如果长度未正确保存,则将元素插入错误的位置:
var arr = ['x', 'y'];
arr.length = 5;
arr.push('z'); // 'z' 被插入到第 5 个索引处,因为这是 “length” 的值
arr; // ['x', 'y', undefined, undefined, undefined, 'z']
采取另一种方法 Array.prototype.join ,Array.prototype.join 还使用 length 属性来确定何时停止连接值:
var arr = ['x', 'y'];
arr.join(); // "x,y"
arr.length = 5;
arr.join(); // "x,y,,,"
Array.prototype.concat 同样适用:
var arr = ['x'];
arr.length = 3;
arr.concat('y'); // ['x', undefined, undefined, 'y']
最后,特殊行为通常在其他情况下被巧妙利用,例如“清除”数组(即删除其所有数字属性):
var arr = [1, 2, 3];
arr.length = 0;
arr; // [] — 将长度设置为0会有效地移除数组的所有数值属性(元素)
现在我们熟悉了这个理论,让我们来看看在实践中对数组进行继承的情况。 这里有几个最受欢迎的:
Andrea Giammarchi解决方案
最近的一个实现是Andrea Giammarchi的 Stack,它看起来像这样:
var Stack = (function () { // (C) Andrea Giammarchi - Mit Style License
function Stack(length) {
if (arguments.length === 1 && typeof length === "number") {
this.length = -1 < length && length === length << 1 >> 1 ? length : this.push(length);
}
else if (arguments.length) {
this.push.apply(this, arguments);
}
};
function Array() { };
Array.prototype = [];
Stack.prototype = new Array;
Stack.prototype.length = 0;
Stack.prototype.toString = function () {
return this.slice(0).toString();
};
Stack.prototype.constructor = Stack;
return Stack;
})();
这是一个有趣的解决方案,它主要针对 Array.prototype.push 和 length 属性的 IE < 8 错误。 但是,现在应该很明显,它并没有真正解决维护长度和数字属性之间关系的问题:
var stack = new Stack('x', 'y');
stack.length; // 2
// 到现在为止还挺好
stack.push('z');
stack.length; // 3
// 还好
stack[3] = 'foo';
stack.length; // 3
// 不是很好(长度应该改为4)
stack.length = 2;
stack[2]; // 'z'
// 仍然不好(第二个索引元素应该被删除)
另一个受欢迎的解决方案是 Dean Edwards。 采用了完全不同的方法 - 不是创建一个从Array.prototype 继承的对象,而是从另一个 iframe 的上下文中“借用”实际的 Array 构造函数。
// 创建一个 <iframe>
var iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
// 将脚本写入 iframe 并窃取其 Array 对象
frames[frames.length - 1].document.write(
"<script>parent.Array2 = Array;<\/script>";
);
这种“有效”的原因是由于浏览器为文档中的每个框架创建单独的执行环境。 每个这样的环境都有一套独立的内置和宿主对象。 内置对象包括全局数组构造函数等。 一个 iframe 的数组对象与另一个 iframe 的数组对象不同。 他们也没有任何种类的等级关系:
// 假设 SubArray 是从另一个 iframe 借用的
var sub = new SubArray(1, 2, 3);
sub instanceof SubArray; // true
sub instanceof Array; // false
sub instanceof Object; // false
注意 sub 为什么不是 Array 的一个实例,也不是 Object 的一个实例。 这是因为 Array 和 Object都不在子对象的原型链中。 相反,原型链包含 SubArray.prototype,接着是来自另一个 iframe 的 Object .prototype:
new SubArray()
|
| [[Prototype]]
|
v
<another iframe>.Array.prototype
|
| [[Prototype]]
|
v
<another iframe>.Object.prototype
|
| [[Prototype]]
|
v
null
这使我们对这种方法有了一个疑问 - 难以确定从这种 iframe 派生的对象的性质。 不再可能使用 instanceof 或构造函数检查来确定对象是数组:
// is this object an array?
sub instanceof Array; // false
sub.constructor === Array; // false
但是,仍然可以使用 [[Class]] 检查(稍后我们将讨论 [[Class]]:
Object.prototype.toString.call(sub) === '[object Array]'; // true
这种方法的另一个比较大的缺点是它不适用于非浏览器环境(或者更确切地说,在任何不支持 iframe 的环境中)。 鉴于服务器端 Javascript 实现速度非常快,这个问题可能会变得更大。
最后,据报道,数组借入可能导致 IE6 中出现混合内容警告,这是其他一些小问题。
除此之外,基于 iframe 的数组 “subclassing” 不存在像 Stack 这样的解决方案的缺点,因为我们处理的是真正的数组对象,并且具有适当的长度/索引关系。
我们来谈谈 ECMAScript 5,正如我在一开始提到的,它带来了一些有助于继承数组的东西。这个“东西”其实不过是属性访问器。这些有用的语言结构已经在一些流行的实现(SpiderMonkey,JavaScriptCore 等)中作为非标准扩展出现了很长一段时间。现在它们已经在新版本实现了。
使用访问器,创建一个具有特殊长度/索引关系的 Object 对象是相当简单的 - 这与 Array 对象的关系相同!而且由于我们已经知道如何在其原型链中创建一个具有 Array.prototype 的对象,所以将这两个方面结合起来就可以完全模拟数组。
有一个关于实施的细节。由于 ECMAScript(包括 last,5th 版本)不提供任何 catch-all(aka noSuchMethod)机制,因此在修改 numeric 属性时无法更改对象的 length 属性值;换句话说,我们不能拦截 '0','1','2','15' 等属性被设置的场景。但是,访问器允许我们截取 length 属性的任何读取访问权限并返回适当的值,具体取决于当时具有哪个数字属性对象。而这是我们真正需要的。
这是它的一个实现,大约有45行代码:
var makeSubArray = (function(){
var MAX_SIGNED_INT_VALUE = Math.pow(2, 32) - 1,
hasOwnProperty = Object.prototype.hasOwnProperty;
function ToUint32(value) {
return value >>> 0;
}
function getMaxIndexProperty(object) {
var maxIndex = -1, isValidProperty;
for (var prop in object) {
isValidProperty = (
String(ToUint32(prop)) === prop &&
ToUint32(prop) !== MAX_SIGNED_INT_VALUE &&
hasOwnProperty.call(object, prop));
if (isValidProperty && prop > maxIndex) {
maxIndex = prop;
}
}
return maxIndex;
}
return function(methods) {
var length = 0;
methods = methods || { };
methods.length = {
get: function() {
var maxIndexProperty = +getMaxIndexProperty(this);
return Math.max(length, maxIndexProperty + 1);
},
set: function(value) {
var constrainedValue = ToUint32(value);
if (constrainedValue !== +value) {
throw new RangeError();
}
for (var i = constrainedValue, len = this.length; i < len; i++) {
delete this[i];
}
length = constrainedValue;
}
};
methods.toString = {
value: Array.prototype.join
};
return Object.create(Array.prototype, methods);
};
})();
我们现在可以通过 makeSubArray 函数创建“子数组”。 它接受一个参数 - 一个带有方法的对象,将其添加到 [[Prototype]] 返回的“子数组”中。
var subMethods = {
last: {
value: function() {
return this[this.length - 1];
}
}
};
var sub = makeSubArray(subMethods);
var sub2 = makeSubArray(subMethods);
// 等等
我们也可以将这个工厂方法隐藏在构造函数的后面,使其与 Array 的类似:
var SubArray = (function () {
var methods = {
last: {
value: function() {
return this[this.length - 1];
}
}
};
return function() {
var arr = makeSubArray(methods);
if (arguments.length === 1) {
arr.length = arguments[0];
}
else {
arr.push.apply(arr, arguments);
}
return arr;
};
})();
然后像使用常规 Array 构造函数一样使用它:
var sub = new SubArray(1, 2, 3);
sub.length; // 3
sub; // [1, 2, 3]
sub.length = 1;
sub; // [1]
sub[10] = 'x';
sub.push(1);
我们刚刚看到利用属性访问器的实现。它不需要任何主机对象(如iframe);它保留长度和数字属性之间的关系;它甚至不允许长度或指数超出范围的值。它只需要支持 ES5(甚至只是Object.create方法)。
但是 [[Class]] 值 - ECMAScript 仍然没有完全控制。
在解释如何检测数组时,我之前曾写过 [[Class]] 。简而言之,[[Class]] 是 ECMAScript 中对象的内部属性。它的值从不直接暴露,但仍可以使用某些方法(例如 Object.prototype.toString)进行检查。 [[Class]]
的用处在于,它允许检测对象的类型,而不依赖于 instanceof 运算符或检查对象的构造函数 - 两者都不足以检测来自其他上下文(例如 iframe)的对象,如前所述。
现在,由于 makeSubArray 创建的对象只是普通的 Object 对象(只有特殊长度的 getter / setter),它们的 [[Class] ]也是 “Object” 而不是 “Array”!我们已经考虑了长度/索引关系,我们设置了 Array.prototype 继承,但是没有办法改变对象的 [[Class]] 值。所以这个解决方案不能说是完美的。
您可能想知道 - 这些数组对象的 [Object] 的 [[Class]] 不是 “Array” 的实际含义是什么。实际上,不能继承[[Class]] 会有不能对象检测的问题。
// assuming that `sub` is a pseudo-array
Object.prototype.toString.call(sub) === '[object Array]'; // false
另一个可能更重要的含义是,ECMAScript 中的一些方法实际上依赖于 [[Class]] 值。 例如,一个众所周知的 Function.prototype.apply 接受一个数组作为它的第二个参数(以及一个参数对象)。 ES3 的 15.3.4.3节说 - “如果 argArray 既不是数组也不是参数对象(见10.1.8),则抛出 TypeError 异常”。 这意味着如果我们传递伪数组对象作为第二个参数来应用它将抛出 TypeError 。 应用程序不知道或关心一个对象是否从
Array.prototype 继承; 它也不关心实现特殊长度/指数行为的对象。 它所关心的只是对象是适当的类型 - 我们很遗憾不能模拟这种类型。
// 假设 `sub` 是一个伪数组
someFunction.apply(this, sub); // TypeError
这方面的规定有些模糊。 例如,在 Date.prototype.setTime spec 中说“如果这个值不是一个 Date 对象,则抛出一个 TypeError 异常”,但在 Date.prototype.getTime 中,它使用 [[Class]] 而不是 “not a Date 对象“ - ”如果此值不是其 [[Class]] 属性为 “Date” 的对象,则引发 TypeError 异常“。
假设这两个短语 - “ Date 对象”和 “Date ['Class] 中的对象”)具有相同的含义可能是安全的。 “ Array 对象”和“ Array [] 的 [[Class]] 对象”以及其他对象也是类似的。
Function.prototype.apply 不是对对象 [[Class]] 敏感的唯一方法。 例如,Array.prototype.concat 基于对象是否为数组(不管是否具有 [[Class] ]“Array”),都遵循不同的算法。
// array ([[Class]] == "Array")
var arr = ['x', 'y'];
// object with numeric properties ([[Class]] == "Object")
var obj = { '0': 'x', '1': 'y' };
[1,2,3].concat(arr); // [1, 2, 3, 'x', 'y']
[1,2,3].concat(obj); // [1, 2, 3, { '0': 'x', '1': 'y' }]
正如你所看到的,数组的值是“扁平的”,而非数组的则保持不变。 当然可以给这些伪数组自定义 concat
实现(并“修复” Array.prototype 上方法中的任何其他方法),但是 Function.prototype.apply 的问题无法解决。
值得一提的是,基于存取器的数组方法的另一个缺点是性能。 我还没有做过任何测试,但很明显,每次访问 length 属性时必须枚举所有数字属性的实现并不会很好。 这就是为什么我不能推荐这个解决方案的原因。
在 Javascript 中实现数组的继承有些徒劳无功,通常会使替代解决方案看起来非常有吸引力。 其中一种解决方案是使用包装。 包装方法避免了设置继承或模拟长度/索引关系。 相反,类似工厂的函数可以创建一个普通的 Array 对象,然后使用任何自定义方法直接对其进行扩充。 由于返回的对象是一个数组,所以它保持适当的长度/索引关系,以及“数组”的 [[Class]] 。 它也自然地从 Array.prototype 继承。
function makeSubArray() {
var arr = [ ];
arr.push.apply(arr, arguments);
arr.last = function() {
return this[this.length - 1];
};
return arr;
}
var sub = makeSubArray(1, 2, 3);
sub instanceof Array; // true
sub.length; // 3
sub.last(); // 3
尽管数组对象的直接扩展是一个美观,简单的解决方案,但它并非没有缺点。 主要缺点是每次调用构造函数时,需要使用 N 个方法来扩展数组。 创建数组所需的时间不再是常量(如果方法在
SubArray.prototype 上),而是与需要添加的方法的数量成正比。
为了克服“N方法”的问题,可以使用包装器的另一种变体 - 其中对象的原型链增加的变体,而不是对象本身。 让我们看看如何做到这一点:
function SubArray() { }
SubArray.prototype = new Array;
SubArray.prototype.last = function() {
return this[this.length - 1];
};
function makeSubArray() {
var arr = [ ];
arr.push.apply(arr, arguments);
arr.__proto__ = SubArray.prototype;
return arr;
}
这个想法很简单。 当执行 makeSubArray 函数时,会发生两件事:
1)创建一个数组对象并使用任何传递的参数填充;
2)对象的原型链以这种方式增加,以便下一个对象是 SubArray.prototype,而不是原始Array.prototype。 原型链的扩充是通过非标准 proto 属性完成的。
但是,makeSubArray 函数中发生的事情当然只是任务的一半。 为了确保该对象在其原型链中具有Array.prototype,我们需要使 SubArray.prototype 从它继承。 这正是这段代码的第二行(SubArray.prototype = new Array)所做的。 从 makeSubArray 返回的对象的原型链如下所示:
new SubArray()
|
| [[Prototype]]
|
v
SubArray.prototype
|
| [[Prototype]]
|
v
Array.prototype
|
| [[Prototype]]
|
v
Object.prototype
|
| [[Prototype]]
|
v
null
因为返回的对象实际上是一个数组,而不是对象,我们也得到长度/指数关系以及适当的 [[Class]] 值。 实际上,我们可以更进一步并将初始化逻辑移入 SubArray 构造函数本身:
function SubArray() {
var arr = [ ];
arr.push.apply(arr, arguments);
arr.__proto__ = SubArray.prototype;
return arr;
}
SubArray.prototype = new Array;
SubArray.prototype.last = function() {
return this[this.length - 1];
};
var sub = new SubArray(1, 2, 3);
sub instanceof SubArray; // true
sub instanceof Array; // true
尽管扩充原型链是一个更高性能的解决方案,但它有一个明显的缺点 - 它依赖于非标准的 proto 属性。 不幸的是,ECMAScript 不允许设置一个对象的 [[Prototype]] - 在其原型链中引用直接祖先的内部属性。 即使在第五版中也没有。 尽管 proto 被大量的实现支持,但它远没有真正兼容。
本篇文章翻译自 http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array,如果不想看我蹩脚的翻译,可以直接查看原文。
Lighthouse 是一个开源的自动化工具,用于改进网络应用的质量。可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。 为 Lighthouse 提供一个审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。目前测试项包括页面性能、PWA、可访问性(无障碍)、最佳实践、SEO。Lighthouse 会对各个测试项的结果打分,并给出优化建议,这些打分标准和优化建议可以视为 Google 的网页最佳实践。
运行 Lighthouse 的方式有两种:
Chrome 扩展程序提供了一个对用户更友好的界面,方便读取报告。命令行工具允许您将 Lighthouse 集成到持续集成系统。
1、下载 Google Chrome 52 或更高版本。
2、安装 Lighthouse Chrome 扩展程序。转到您要进行审查的页面。
3、点击位于 Chrome 工具栏上的 Lighthouse 图标 (Lighthouse 图标)。
4、Lighthouse 菜单。如果您想仅运行审查的子集,则点击 Options 按钮并停用您不关注的审查。 向下滚动并按 OK 以确认您的更改。
5、Lighthouse 选项菜单。点击 Generate report 按钮以针对当前打开的页面运行 Lighthouse 测试。在完成审查后,Lighthouse 将打开一个新标签,并在页面的结果上显示一个报告。
Lighthouse 报告
安装 Node,需要版本 5 或更高版本。安装 Lighthouse 作为一个全局节点模块。
npm install -g lighthouse
针对一个页面运行 Lighthouse 审查。
lighthouse https://airhorner.com/
传递 --help 标志以查看可用的输入和输出选项。
lighthouse --help
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本事的节点和一个指向下一个元素的引用组成。相对于传统的数组,链表的一个好处在于,添加或者删除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。
数组和链表的一个不同在于数组可以直接访问任何位置的元素,而想要访问链表中的一个元素,需要从起点开始迭代列表。
下面是单向链表的具体实现代码:
function LinkedList() {
var Node = function(element){
this.element = element;
this.next = null;
};
var length = 0;//链表长度
var head = null;//第一个节点
this.append = function(element){
var node = new Node(element),
current;
if (head === null){ //列表为空
head = node;
} else { //列表不为空
current = head; //现在只知道第一项head
while(current.next){ //找到列表的最后一项
current = current.next;
}
//建立链接
current.next = node;
}
length++; //更新列表长度
};
this.insert = function(position, element){
//检查越界值
if (position >= 0 && position <= length){
var node = new Node(element),
current = head,
previous,
index = 0;
if (position === 0){ //在第一个位置添加
node.next = current;
head = node;
} else { //在中间或者尾部添加
while (index++ < position){
previous = current;
current = current.next;
}
node.next = current; //先连上添加的节点
previous.next = node; //再断开之前的连接
}
length++;
return true;
} else {
return false;
}
};
this.removeAt = function(position){
if (position > -1 && position < length){
var current = head,
previous,
index = 0; //用来迭代列表,直到到达目标位置
if (position === 0){ //移除第一项
head = current.next;
} else { //移除中间或者尾部最后一项
while (index++ < position){
previous = current;
current = current.next;
}
//连接前一项和后一项,跳过当前的项,相当于移除了当前项
previous.next = current.next;
}
length--;
return current.element;
} else {
return null;
}
};
this.remove = function(element){
var index = this.indexOf(element);
return this.removeAt(index);
};
this.indexOf = function(element){
var current = head,
index = 0;
while (current) {
if (element === current.element) {
return index;
}
index++; //记录位置
current = current.next;
}
return -1;
};
this.isEmpty = function() {
return length === 0;
};
this.size = function() {
return length;
};
this.getHead = function(){
return head;
};
this.toString = function(){
var current = head,
string = '';
while (current) {
string += current.element;//拼接
current = current.next;
}
return string;
};
this.print = function(){
console.log(this.toString());
};
}
双向链表和单向链表的区别在于,在单向链表中,一个节点只有链向下一个节点的链接。而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素。示例代码如下:
function DoublyLinkedList() {
var Node = function(element){
this.element = element;
this.next = null;
this.prev = null; //新添加的
};
var length = 0;
var head = null;
var tail = null; //新添加的
this.append = function(element){
var node = new Node(element),
current;
if (head === null){ //列表为空
head = node;
tail = node;
} else {
tail.next = node;
node.prev = tail;
tail = node;
}
length++;
};
this.insert = function(position, element){
if (position >= 0 && position <= length){
var node = new Node(element),
current = head,
previous,
index = 0;
if (position === 0){ //在第一个位置
if (!head){ //列表为空
head = node;
tail = node;
} else { //列表不为空
node.next = current;
current.prev = node;
head = node;
}
} else if (position === length) { //最后一项
current = tail;
current.next = node;
node.prev = current;
tail = node;
} else {
while (index++ < position){
previous = current;
current = current.next;
}
node.next = current;
previous.next = node; //把node节点连接进去前一个节点和后一个节点
current.prev = node; //断掉之前previous和current的连接
node.prev = previous; //prev同样需要连接
}
length++;
return true;
} else {
return false;
}
};
this.removeAt = function(position){
if (position > -1 && position < length){
var current = head,
previous,
index = 0;
if (position === 0){ //移除第一项
head = current.next;
if (length === 1){ // 列表只有一项
tail = null;
} else {
head.prev = null;
}
} else if (position === length-1){ 移除最后一项
current = tail; // {4}
tail = current.prev;
tail.next = null;
} else {
while (index++ < position){
previous = current;
current = current.next;
}
previous.next = current.next; // 链接前一项和后一项,跳过当前项
current.next.prev = previous; //修复prev
}
length--;
return current.element;
} else {
return null;
}
};
this.remove = function(element){
var index = this.indexOf(element);
return this.removeAt(index);
};
this.indexOf = function(element){
var current = head,
index = -1;
//检查第一项
if (element == current.element){
return 0;
}
index++;
//检查中间项
while(current.next){
if (element == current.element){
return index;
}
current = current.next;
index++;
}
//检查最后一项
if (element == current.element){
return index;
}
return -1;
};
this.isEmpty = function() {
return length === 0;
};
this. size = function() {
return length;
};
this.toString = function(){
var current = head,
s = current ? current.element : '';
while(current && current.next){
current = current.next;
s += ', ' + current.element;
}
return s;
};
this.inverseToString = function() {
var current = tail,
s = current ? current.element : '';
while(current && current.prev){
current = current.prev;
s += ', ' + current.element;
}
return s;
};
this.print = function(){
console.log(this.toString());
};
this.printInverse = function(){
console.log(this.inverseToString());
};
this.getHead = function(){
return head;
};
this.getTail = function(){
return tail;
}
}
循环链表可以像单向链表那样只有单向引用,也可以像双向链表那样有双向引用。循环链表和其他链表的区别在于最后一个元素指向下一个元素的引用不是null,而是指向第一个元素(head)。示例代码如下:
function CircularLinkedList() {
var Node = function(element){
this.element = element;
this.next = null;
};
var length = 0;
var head = null;
this.append = function(element){
var node = new Node(element),
current;
if (head === null){ //列表为空
head = node;
} else {
current = head;
while(current.next !== head){ //最后一个元素将是head,而不是null
current = current.next;
}
current.next = node; //建立连接
}
node.next = head; //首尾相连起来变成一个环列表
length++;
};
this.insert = function(position, element){
if (position >= 0 && position <= length){
var node = new Node(element),
current = head,
previous,
index = 0;
if (position === 0){ //在第一项
node.next = current;
while(current.next !== head){
current = current.next;
}
head = node;
current.next = head;
} else {
while (index++ < position){
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
if (node.next === null){ //在最后一个元素更新
node.next = head;
}
}
length++;
return true;
} else {
return false;
}
};
this.removeAt = function(position){
if (position > -1 && position < length){
var current = head,
previous,
index = 0;
if (position === 0){
while(current.next !== head){
current = current.next;
}
head = head.next;
current.next = head; //更新最后一项
} else {
while (index++ < position){
previous = current;
current = current.next;
}
previous.next = current.next;
}
length--;
return current.element;
} else {
return null;
}
};
this.remove = function(element){
var index = this.indexOf(element);
return this.removeAt(index);
};
this.indexOf = function(element){
var current = head,
index = -1;
if (element == current.element){ //检查第一项
return 0;
}
index++;
while(current.next !== head){ //检查列表中间
if (element == current.element){
return index;
}
current = current.next;
index++;
}
if (element == current.element){ //检查最后一项
return index;
}
return -1;
};
this.isEmpty = function() {
return length === 0;
};
this.size = function() {
return length;
};
this.getHead = function(){
return head;
};
this.toString = function(){
var current = head,
s = current.element;
while(current.next !== head){
current = current.next;
s += ', ' + current.element;
}
return s.toString();
};
this.print = function(){
console.log(this.toString());
};
}
/**
* slice() reference.
*/
var slice = Array.prototype.slice;
/**
* Expose `co`.
*/
module.exports = co['default'] = co.co = co;
/**
* Wrap the given generator `fn` into a
* function that returns a promise.
* This is a separate function so that
* every `co()` call doesn't create a new,
* unnecessary closure.
*
* @param {GeneratorFunction} fn
* @return {Function}
* @api public
*/
// 有参数的 generator 调用
// var fn = co.wrap(function* (val) {
// return yield Promise.resolve(val);
// });
// fn(true).then(function (val) {
// console.log(val); // true
// });
co.wrap = function (fn) {
// 存了一个指针指向原 generator 函数
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
/**
* Execute the generator function or a generator
* and return a promise.
*
* @param {Function} fn
* @return {Promise}
* @api public
*/
function co(gen) {
// 缓存 this
var ctx = this;
// 获取 co 函数从第二个参数到最后一个参数,除 gen 之外的其他参数
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
// 返回的是 Promise(这是为什么可以用 then 和 catch 的根源)
return new Promise(function(resolve, reject) {
// 执行外部传入的 gen
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 如果不具备 next 属性或者不是一个函数
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
// 为了能够捕捉抛出的错误
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res); // generator 函数指针移动到了一个位置
} catch (e) {
return reject(e);
}
next(ret); // 反复调用
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
// 如果执行完成,直接调用 resolve 把 promise 置为成功状态
if (ret.done) return resolve(ret.value);
// 将 ret 的 value 转换为 Promise 形式
var value = toPromise.call(ctx, ret.value);
// 直接给新的 promise 添加 onFulfilled, onRejected
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 抛出错误,yield 后只能跟着指定的下列这几种类型
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
/**
* Convert a `yield`ed value into a promise.
*
* @param {Mixed} obj
* @return {Promise}
* @api private
*/
function toPromise(obj) {
// obj 不存在,直接返回
if (!obj) return obj;
// 如果 obj 已经是 Promise,直接返回
if (isPromise(obj)) return obj;
// 如果是个 generator 函数或者 generator 生成器,执行
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
// 如果是个普通的函数(需要符合thunk函数规范),就将该函数包装成 Promise 的形式
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
// 如果 obj 是 array,直接用 Promise.all 包装
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
// 如果 obj 是 object
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
/**
* Convert a thunk to a promise.
*
* @param {Function}
* @return {Promise}
* @api private
*/
// thunk 函数具备以下两个要素:
// 有且只有一个参数是 callback 的函数
// callback 的第一个参数是 error
function thunkToPromise(fn) {
var ctx = this;
// 将 thunk 函数包装成 Promise
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}
/**
* Convert an array of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Array} obj
* @return {Promise}
* @api private
*/
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
/**
* Convert an object of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Object} obj
* @return {Promise}
* @api private
*/
function objectToPromise(obj) {
// 构造一个和传入对象有相同构造器的对象
var results = new obj.constructor();
// 获取 obj 的keys
var keys = Object.keys(obj);
// 存储 obj 中是 Promise 的属性
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// 转换为 Promise 形式
var promise = toPromise.call(this, obj[key]);
// 如果是结果是 Promise,则用 defer 函数对 results 的某个 Promise 返回值进行修改
if (promise && isPromise(promise)) defer(promise, key);
// 如果不是就直接返回
else results[key] = obj[key];
}
// 等待所有 promise 执行完毕,返回结果
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
// 预定义
results[key] = undefined;
promises.push(promise.then(function (res) {
// 运行成功结果赋值给 results
results[key] = res;
}));
}
}
/**
* Check if `obj` is a promise.
*
* @param {Object} obj
* @return {Boolean}
* @api private
*/
function isPromise(obj) {
return 'function' == typeof obj.then;
}
/**
* Check if `obj` is a generator.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
/**
* Check if `obj` is a generator function.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}
/**
* Check for plain object.
*
* @param {Mixed} val
* @return {Boolean}
* @api private
*/
function isObject(val) {
return Object == val.constructor;
}
能力检测,又可以称为特性检测,它的目标是识别浏览器的能力,它的基本模式如下:
if (object.property) {
// 使用 object.property
}
在浏览器中可以采用 JavaScript 检测是否支持 WebP ,对支持 WebP 的用户输出 WebP 图片,否则输出其他格式的图片。
// 简化写法
function isSupportWebp() {
var isSupportWebp = false;
try {
isSupportWebp = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') == 0;
} catch(err) {
console.log(err);
}
return isSupportWebp;
}
上面代码的问题在于我们每当使用一次 isSupportWebp 时都会进行一次判断。
为了防止每次调用都判断的问题,我们可以用一个变量来作缓存。
function isSupportWebp() {
var isSupportWebp = false;
try {
isSupportWebp = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') == 0;
} catch(err) {
console.log(err);
}
return isSupportWebp;
}
var isSupportWebpTmp = isSupportWebp();
这样我们可以使用 isSupportWebpTmp 来判定浏览器是否支持 WebP 格式的图片,但缺点是多了一个变量来做缓存,代码有点冗余。
我们也可以使用立即执行的匿名函数的形式来进行判断。
var isSupportWebp = (function () {
var isSupportWebp = false;
try {
isSupportWebp = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') == 0;
} catch(err) {
console.log(err);
}
return isSupportWebp;
})();
以上代码缺点是多了一个立即执行的匿名函数,当我在声明的时候,这个函数已经执行过一遍了。那有没有一种方法是在运行时才执行呢?
答案是肯定的。这时候可以利用惰性函数,代码如下:
// 惰性函数
function isSupportWebp() {
var isSupportWebpTmp = false;
try {
isSupportWebpTmp = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') == 0;
} catch(err) {
console.log(err);
}
isSupportWebp = function () {
return isSupportWebpTmp;
}
return isSupportWebp();
};
当我们每次都需要进行条件判断,其实只需要判断一次,接下来的使用方式都不会发生改变的时候,想想是否可以考虑使用惰性函数,能力检测只是其中一种类型。
集合是由一组无序且唯一的项组成的。这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。在数学中,集合也有并集、交集、差集等基本操作,在下面的代码中也会实现这些操作。
值的相等:因为 Set 中的值总是唯一的,所以需要判断两个值是否相等。判断相等的算法与严格相等(===操作符)不同。具体来说,对于 Set , +0 (+0 严格相等于-0)和-0是不同的值。尽管在最新的 ECMAScript 6规范中这点已被更改。从Gecko 29.0和 recent nightly Chrome开始,Set 视 +0 和 -0 为相同的值。另外,NaN和undefined都可以被存储在Set 中, NaN之间被视为相同的值(尽管 NaN !== NaN)。
function Set() {
var items = {};
this.has = function(value){ //判定值是否在集合中
return items.hasOwnProperty(value);
};
this.add = function(value){ //向集合添加一个新的项
if (!this.has(value)){
items[value] = value;
return true;
}
return false;
};
this.remove = function(value){ //从集合移除一个值
if (this.has(value)){
delete items[value];
return true;
}
return false;
};
this.clear = function(){ //清空集合
items = {};
};
this.size = function(){ //集合元素的个数
var count = 0;
for(var prop in items) {
if(items.hasOwnProperty(prop))
++count;
}
return count;
};
this.values = function(){ //集合所有值的数组
var keys = [];
for(var key in items){
keys.push(key);
}
return keys;
};
this.getItems = function(){ //获取集合
return items;
};
this.union = function(otherSet){ //并集
var unionSet = new Set();
var values = this.values();
for (var i=0; i<values.length; i++){
unionSet.add(values[i]);
}
values = otherSet.values();
for (var i=0; i<values.length; i++){
unionSet.add(values[i]);
}
return unionSet;
};
this.intersection = function(otherSet){ //交集
var intersectionSet = new Set();
var values = this.values();
for (var i=0; i<values.length; i++){
if (otherSet.has(values[i])){
intersectionSet.add(values[i]);
}
}
return intersectionSet;
};
this.difference = function(otherSet){ //差集
var differenceSet = new Set();
var values = this.values();
for (var i=0; i<values.length; i++){
if (!otherSet.has(values[i])){
differenceSet.add(values[i]);
}
}
return differenceSet;
};
this.subset = function(otherSet){ //子集
if (this.size() > otherSet.size()){ //子集的元素个数要小于otherSet的元素个数
return false;
} else {
var values = this.values();
for (var i=0; i<values.length; i++){
if (!otherSet.has(values[i])){
return false; //有一个没有返回false
}
}
return true;
}
};
}
在讲DOM操作的最佳性能实践之前,先介绍下浏览器的基本渲染原理。浏览器渲染展示网页的主流程大致可以用下图表示:
分为以下四个步骤:
解析HTML(HTML Parser)
构建DOM树(DOM Tree)
渲染树构建(Render Tree)
绘制渲染树(Painting)
浏览器请求解析(Parser) HTML 文档,并将各标记逐个转化成 DOM 节点(DOM Tree)。同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于创建另一个树结构:呈现树(Render Tree)。呈现树(Render Tree)包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。呈现树(Render Tree)构建完毕之后,进入“布局”处理阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标。下一个阶段是绘制(Painting) - 浏览器会遍历呈现树(Render Tree),由用户界面后端层将每个节点绘制出来。
Repaint:可以理解为重绘或重画,当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,例如改变背景颜色 。则就叫称为重绘。
Reflows:可以理解为回流、布局或者重排,当渲染树(render Tree)中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow),也就是重新布局(relayout)。
改变用于构建渲染树的任何内容都可能导致重绘或回流,例如:
1、添加,删除,更新DOM节点
2、用display: none(回流和重绘)或者visibility: hidden隐藏节点(只有重绘,因为没有几何更改)
3、添加样式表,调整样式属性
4、调整窗口大小,更改字体大小
5、页面初始化的渲染
6、移动DOM元素
。。。
我们来看几个例子:
var bstyle = document.body.style; // cache
bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // another reflow and a repaint
bstyle.color = "blue"; // repaint only, no dimensions changed
bstyle.backgroundColor = "#fad"; // repaint
bstyle.fontSize = "2em"; // reflow, repaint
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));
我们可以想象一下,如果直接在渲染树(render Tree)最后面增加或者删除一个节点,这对于浏览器渲染页面来说无伤大雅,因为只需要在渲染树(render Tree)的末端重绘那一部分变动的节点。但是,如果是在页面的顶部变动一个节点,浏览器需要重新计算渲染树(render Tree),导致渲染树(render Tree)的一部分或全部发生变化。渲染树(render Tree)重新建立后,浏览器会重新绘制页面上受影响的元素。重排的代价比重绘的代价高很多,重绘会影响部分的元素,而重排则有可能影响全部的元素。
DOM操作带来的页面 Repaints 和 Reflows 是不可避免的,但可以遵循一些最佳实践来最大限度地减少Repaints 和 Reflows。如下是一些具体的实践方法:
// bad
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// better
el.className += " theclassname";
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
由于与渲染树更改相关的 Repaints and Reflows 是代价非常高,因此现代浏览器针对频繁的 Repaints and Reflows 有性能的优化。 一个策略是浏览器将设置脚本所需更改的队列,并分批执行。 这样,每个需要 Reflows 的几个变化将被组合,并且将仅计算一个 Reflows 。 浏览器可以添加排队的更改,然后在一定时间过去或达到一定数量的更改后刷新队列(并不是所有的浏览器都存在这样的优化。推荐的方式是把DOM操作尽量合并)。但有时脚本可能会阻止浏览器优化 Reflows ,并使其刷新队列并执行所有批量更改。 当您请求如下样式信息时(并非包含全部),会发生这种情况。见下图:
以上所有这些基本上都是请求有关节点的样式信息,浏览器必须提供最新的值。 为了做到这一点,它需要应用所有计划的更改,刷新队列,强行回流。所以在有大批量DOM操作时,应避免获取DOM元素的布局信息,使得浏览器针对大批量DOM操作的优化不被破坏。如果需要这些布局信息,最好是在DOM操作之前就去获取。
//bad
var bstyle = document.body.style;
bodystyle.color = 'red';
tmp = computed.backgroundColor;
bodystyle.color = 'white';
tmp = computed.backgroundImage;
bodystyle.color = 'green';
tmp = computed.backgroundAttachment;
//better
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;
bodystyle.color = 'yellow';
bodystyle.color = 'pink';
bodystyle.color = 'blue';
(1)使用文档片段
DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的孩子所代替。因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(Reflow)。当然,最后一步把文档片段附加到页面的这一步操作还是会造成回流(Reflow)。
var fragment = document.createDocumentFragment();
// 一些基于fragment的大量DOM操作
...
document.getElementById('myElement').appendChild(fragment);
(2)通过设置DOM元素的display样式为none来隐藏元素
原理是先隐藏元素,然后基于元素做DOM操作,经过大量的DOM操作后才把元素显示出来。
var myElement = document.getElementById('myElement');
myElement.style.display = 'none';
// 一些基于myElement的大量DOM操作
...
myElement.style.display = 'block';
(3)克隆DOM元素到内存中
这种方式是把页面上的DOM元素克隆一份到内存中,然后再在内存中操作克隆的元素,操作完成后使用此克隆元素替换页面中原来的DOM元素。
var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
// 一些基于clone的大量DOM操作
...
old.parentNode.replaceChild(clone, old);
获取DOM的样式信息会有性能的损耗,所以如果存在循环调用,最佳的做法是尽量把这些值缓存在局部变量中。
// bad
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
// better
var width = box.offsetWidth;
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
使用绝对定位使得该元素在渲染树中成为 body 下的一个直接子节点,因此当它进行动画时,它不会影响太多其他节点。
以下会通过一个具体例子来说明,链接地址如下:reflow。
第一次点击的代码如下:
function touch() {
bodystyle.color = 'red';
bodystyle.padding = '1px';
tmp = computed.backgroundColor;
bodystyle.color = 'white';
bodystyle.padding = '2px';
tmp = computed.backgroundImage;
bodystyle.color = 'green';
bodystyle.padding = '3px';
tmp = computed.backgroundAttachment;
}
第二次点击的代码如下:
function touchlast() {
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;
bodystyle.color = 'yellow';
bodystyle.padding = '4px';
bodystyle.color = 'pink';
bodystyle.padding = '5px';
bodystyle.color = 'blue';
bodystyle.padding = '6px';
}
以下我们将通过谷歌工具来查看这两次操作有什么异同。
结果如下图:
结果如下图:
结果如下图,注意箭头指的地方:
从上图我们可以很容易看到在点击body的过程中,浏览器计算了3次样式。
结果如下图,注意箭头指的地方:
从上图我们可以很容易看到在再次点击body的过程中,浏览器只计算了1次样式。从而可以证明我们上述的浏览器批处理的结论。未优化的Rendering(渲染)时间为0.4ms,而优化后的Rendering(渲染)时间为0.3ms,在这么小的js执行都有这么大的差别,在一些操作DOM频繁的动画中,浪费的性能可想而知,后面将会有一个动画的例子可以直观的看出来。
谷歌文档给的例子,链接地址如下:animation。
优化前的代码:
var pos = m.classList.contains('down') ?
m.offsetTop + distance : m.offsetTop - distance;
if (pos < 0) pos = 0;
if (pos > maxHeight) pos = maxHeight;
m.style.top = pos + 'px';
if (m.offsetTop === 0) {
m.classList.remove('up');
m.classList.add('down');
}
if (m.offsetTop === maxHeight) {
m.classList.remove('down');
m.classList.add('up');
}
优化后的代码:
var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px')));
m.classList.contains('down') ? pos += distance : pos -= distance;
if (pos < 0) pos = 0;
if (pos > maxHeight) pos = maxHeight;
m.style.top = pos + 'px';
if (pos === 0) {
m.classList.remove('up');
m.classList.add('down');
}
if (pos === maxHeight) {
m.classList.remove('down');
m.classList.add('up');
}
先节流cpu,然后加多小“谷歌”图标,直到图标速度明显减慢,再点击“Optimize”优化按钮,可以明显感受出差距。至于如何节流cpu及定位问题可以参考我的另外一篇文章什么?页面卡顿?操作慢?。
栈是一种遵从后进先出(LIFO)原则的有序集合。新添加的或待删除的元素都保存在栈的末尾。称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都靠近栈底。现在通过数组的方法来实现栈,代码如下:
function Stack() {
var items = [];
this.push = function(element){//添加一个(或几个)新元素到栈顶
items.push(element);
};
this.pop = function(){//移除栈顶的元素,同时返回被移除元素
return items.pop();
};
this.peek = function(){//返回栈顶的元素,但并不对栈做任何修改
return items[items.length-1];
};
this.isEmpty = function(){//如果栈内没有任何元素就返回true,否则返回false
return items.length == 0;
};
this.size = function(){//返回栈里的元素个数
return items.length;
};
this.clear = function(){//移除栈里的所有元素
items = [];
};
this.print = function(){//打印
console.log(items.toString());
};
this.toString = function(){
return items.toString();
};
}
下面是一个小算法题,可以视为栈的综合利用,如何将10进制数字转成2进制数字:
function divideBy2(decNumber){
var remStack = new Stack(),
rem,
binaryString = "";
while(decNumber > 0){
rem = Math.floor(decNumber % 2);
remStack.push(rem);
decNumber = Math.floor(decNumber / 2);
}
while(!remStack.isEmpty()){
binaryString += remStack.pop().toString();//余数除完翻转过来就是2进制数
}
return binaryString;
}
升级版, 如何将10进制数字转成任意进制数字,代码如下:
function baseConverter(decNumber,base){
var remStack = new Stack(),
rem,
baseString = "",
digits = "0123456789ABCDEF";
while(decNumber > 0){
rem = Math.floor(decNumber % base);
remStack.push(rem);
decNumber = Math.floor(decNumber / base);
}
while(!remStack.isEmpty()){
baseString += digits[remStack.pop()];
}
return baseString;
}
baseConverter(100345,2) // "11000011111111001"
baseConverter(100345,8) //"303771"
baseConverter(100345,16) // "187F9"
队列遵循的是FIFO(先进先出)的原则的一组有序的项。队列从尾部添加新元素,并从顶部移除元素,最新添加的元素必须排列在队列的末尾。
function Queue() {
var items = [];
this.enqueue = function(element){//向队列尾部添加一个(或是多个)元素
items.push(element);
};
this.dequeue = function(){//移除队列的第一个元素,并返回被移除的元素
return items.shift();
};
this.front = function(){//返回队列的第一个元素——最先被添加的,也将是最先被移除的元素。队列不做任何变动。(不移除元素,只返回元素信息。与stack的peek方法类似)
return items[0];
};
this.isEmpty = function(){//如果队列内没有任何元素就返回true,否则返回false
return items.length == 0;
};
this.clear = function(){//移除队列里的所有元素
items = [];
};
this.size = function(){//返回队列里的元素个数
return items.length;
};
this.print = function(){//打印
console.log(items.toString());
};
}
指队列元素的添加和移除是基于优先级的。实现一个优先队列,有两种选项:设置优先级,然后再正确的位置添加元素;或者用入队操作添加元素,然后按照优先级移除他们。下例将会在正确的位置添加元素,如下:
function PriorityQueue(){
var items = [];
function QueueElement(element, priority){
this.element = element;
this.priority = priority;
}
this.enqueue = function(element, priority){
var queueElement = new QueueElement(element, priority);
if(this.isEmpty()){
items.push(queueElement);
}else{
var added = false;
for(var i = 0; i < items.length; i++){
if(queueElement.priority < items[i].priority){
items.splice(i,0,queueElement);
added = true;
break;
}
}
}
if(!added){
items.push(queueElement);
}
}
this.dequeue = function(){
return items.shift();
};
this.front = function(){
return items[0];
};
this.isEmpty = function(){
return items.length == 0;
};
this.size = function(){
return items.length;
};
this. print = function(){
for (var i=0; i<items.length; i++){
console.log(items[i].element + ' - ' + items[i].priority);
}
};
}
击鼓传花游戏,在这个游戏中,孩子们围成一个圆圈,把花尽快的传递给旁边的人。某一时刻传花停止,这个时候花落在谁手里,谁就退出圆圈结束游戏。重复这个过程,直到只剩下一个孩子。例子如下:
function hotPotato(namelist, num){
var queue = new Queue();
for(var i = 0; i < namelist.length; i++){
queue.enqueue(namelist[i]);
}
var eliminated = '';
while(queue.size() > 1){
for(var i = 0; i < num; i++){
queue.enqueue(queue.dequeue());
}
eliminated = queue.dequeue();
console.log(eliminated+"在游戏中淘汰了。");
}
return queue.dequeue();
}
var names = ["a","b","c","d","e"];
var winner = hotPotato(names,7);
console.log("胜利者"+winner);
//c在游戏中淘汰了。
//b在游戏中淘汰了。
//e在游戏中淘汰了。
//d在游戏中淘汰了。
//胜利者a
绝对定位与负边距实现。利用绝对定位,将元素的top和left属性都设为50%,再利用margin边距,将元素回拉它本身高宽的一半,实现垂直居中。代码如下:
#container {
position:relative;
}
#div {
position:absolute;
width: x;
height: y;
top: 50%;
left: 50%;
margin: -x / 2 0 0 -y / 2;
}
也是利用绝对定位与margin。代码如下:
#container {
position:relative;
}
#div {
position: absolute;
margin: 0 auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
当要被居中的元素是内联元素的时候,可以巧妙的将父级容器设置为display:table-cell,配合text-align:center和vertical-align:middle即可以实现水平垂直居中。代码如下:
#container {
display:table-cell;
text-align:center;
vertical-align:middle;
}
利用Css3的transform,可以轻松的在未知元素的宽高的情况下实现元素的垂直居中。代码如下:
#container {
position:relative;
}
#div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
使用flex布局,无需绝对定位等改变布局的操作,可以轻松实现元素的水平垂直居中。代码如下:
#container {
display:flex;
justify-content:center;
align-items: center;
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.