本篇是看的《JS高级程序设计》第23章《高级技巧》作的读书分享。本篇按照书里的思路根据本身的理解和经验,进行扩展延伸,同时指出书里的一些问题。将会讨论安全的类型检测、惰性载入函数、冻结对象、定时器等话题。javascript
这个问题是怎么安全地检测一个变量的类型,例如判断一个变量是否为一个数组。一般的作法是使用instanceof,以下代码所示:html
let data = [1, 2, 3];
console.log(data instanceof Array); //true复制代码
可是上面的判断在必定条件下会失败——就是在iframe里面判断一个父窗口的变量的时候。写个demo验证一下,以下主页面的main.html:java
<script> window.global = { arrayData: [1, 2, 3] } console.log("parent arrayData installof Array: " + (window.global.arrayData instanceof Array)); </script> <iframe src="iframe.html"></iframe>复制代码
在iframe.html判断一下父窗口的变量类型:跨域
<script> console.log("iframe window.parent.global.arrayData instanceof Array: " + (window.parent.global.arrayData instanceof Array)); </script> 复制代码
在iframe里面使用window.parent获得父窗口的全局window对象,这个无论跨不跨域都没有问题,进而能够获得父窗口的变量,而后用instanceof判断。最后运行结果以下:数组
能够看到父窗口的判断是正确的,而子窗口的判断是false,所以一个变量明明是Array,但却不是Array,这是为何呢?既然这个是父子窗口才会有的问题,因而试一下把Array改为父窗口的Array,即window.parent.Array,以下图所示:缓存
此次返回了true,而后再变换一下其它的判断,如上图,最后能够知道根本缘由是上图最后一个判断:安全
Array !== window.parent.Arraycookie
它们分别是两个函数,父窗口定义了一个,子窗口又定义了一个,内存地址不同,内存地址不同的Object等式判断不成立,而window.parent.arrayData.constructor返回的是父窗口的Array,比较的时候是在子窗口,使用的是子窗口的Array,这两个Array不相等,因此致使判断不成立。多线程
那怎么办呢?闭包
因为不能使用Object的内存地址判断,可使用字符串的方式,由于字符串是基本类型,字符串比较只要每一个字符都相等就行了。ES5提供了这么一个方法Object.prototype.toString,咱们先小试牛刀,试一下不一样变量的返回值:
能够看到若是是数组返回"[object Array]",ES5对这个函数是这么规定的:
也就是说这个函数的返回值是“[object ”开头,后面带上变量类型的名称和右括号。所以既然它是一个标准语法规范,因此能够用这个函数安全地判断变量是否是数组。
能够这么写:
Object.prototype.toString.call([1, 2, 3]) ===
"[object Array]"复制代码
注意要使用call,而不是直接调用,call的第一个参数是context执行上下文,把数组传给它做为执行上下文。
有一个比较有趣的现象是ES6的class也是返回function:
因此能够知道class也是用function实现的原型,也就是说class和function本质上是同样的,只是写法上不同。
那是否是说不能再使用instanceof判断变量类型了?不是的,当你须要检测父页面的变量类型就得使用这种方法,本页面的变量仍是可使用instanceof或者constructor的方法判断,只要你能确保这个变量不会跨页面。由于对于大多数人来讲,不多会写iframe的代码,因此没有必要搞一个比较麻烦的方式,仍是用简单的方式就行了。
有时候须要在代码里面作一些兼容性判断,或者是作一些UA的判断,以下代码所示:
//UA的类型
getUAType: function() {
let ua = window.navigator.userAgent;
if (ua.match(/renren/i)) {
return 0;
}
else if (ua.match(/MicroMessenger/i)) {
return 1;
}
else if (ua.match(/weibo/i)) {
return 2;
}
return -1;
}复制代码
这个函数的做用是判断用户是在哪一个环境打开的网页,以便于统计哪一个渠道的效果比较好。
这种类型的判断都有一个特色,就是它的结果是死的,无论执行判断多少次,都会返回相同的结果,例如用户的UA在这个网页不可能会发生变化(除了调试设定的以外)。因此为了优化,才有了惰性函数一说,上面的代码能够改为:
//UA的类型
getUAType: function() {
let ua = window.navigator.userAgent;
if(ua.match(/renren/i)) {
pageData.getUAType = () => 0;
return 0;
}
else if(ua.match(/MicroMessenger/i)) {
pageData.getUAType = () => 1;
return 1;
}
else if(ua.match(/weibo/i)) {
pageData.getUAType = () => 2;
return 2;
}
return -1;
}
复制代码
在每次判断以后,把getUAType这个函数从新赋值,变成一个新的function,而这个function直接返回一个肯定的变量,这样之后的每次获取都不用再判断了,这就是惰性函数的做用。你可能会说这么几个判断能优化多少时间呢,这么点时间对于用户来讲几乎是没有区别的呀。确实如此,可是做为一个有追求的码农,仍是会想办法尽量优化本身的代码,而不是只是为了完成需求完成功能。而且当你的这些优化累积到一个量的时候就会发生质变。我上大学的时候C++的老师举了一个例子,说有个系统比较慢找她去看一下,其中她作的一个优化是把小数的双精度改为单精度,最后是快了很多。
但其实上面的例子咱们有一个更简单的实现,那就是直接搞个变量存起来就行了:
let ua = window.navigator.userAgent;
let UAType = ua.match(/renren/i) ? 0 :
ua.match(/MicroMessenger/i) ? 1 :
ua.match(/weibo/i) ? 2 : -1;复制代码
连函数都不用写了,缺点是即便没有使用到UAType这个变量,也会执行一次判断,可是咱们认为这个变量被用到的几率仍是很高的。
咱们再举一个比较有用的例子,因为Safari的无痕浏览会禁掉本地存储,所以须要搞一个兼容性判断:
Data.localStorageEnabled = true;
// Safari的无痕浏览会禁用localStorage
try{
window.localStorage.trySetData = 1;
} catch(e) {
Data.localStorageEnabled = false;
}
setLocalData: function(key, value) {
if (Data.localStorageEnabled) {
window.localStorage[key] = value;
}
else {
util.setCookie("_L_" + key, value, 1000);
}
}
复制代码
在设置本地数据的时候,须要判断一下是否是支持本地存储,若是是的话就用localStorage,不然改用cookie。能够用惰性函数改造一下:
setLocalData: function(key, value) {
if(Data.localStorageEnabled) {
util.setLocalData = function(key, value){
return window.localStorage[key];
}
} else {
util.setLocalData = function(key, value){
return util.getCookie("_L_" + key);
}
}
return util.setLocalData(key, value);
}
复制代码
这里能够减小一次if/else的判断,但好像不是特别实惠,毕竟为了减小一次判断,引入了一个惰性函数的概念,因此你可能要权衡一下这种引入是否值得,若是有三五个判断应该仍是比较好的。
有时候要把一个函数看成参数传递给另外一个函数执行,此时函数的执行上下文每每会发生变化,以下代码:
class DrawTool {
constructor() {
this.points = [];
}
handleMouseClick(event) {
this.points.push(event.latLng);
}
init() {
$map.on('click', this.handleMouseClick);
}
}
复制代码
click事件的执行回调里面this不是指向了DrawTool的实例了,因此里面的this.points将会返回undefined。第一种解决方法是使用闭包,先把this缓存一下,变成that:
class DrawTool {
constructor() {
this.points = [];
}
handleMouseClick(event) {
this.points.push(event.latLng);
}
init() {
let that = this;
$map.on('click', event => that.handleMouseClick(event));
}
}复制代码
因为回调函数是用that执行的,而that是指向DrawTool的实例子,所以就没有问题了。相反若是没有that它就用的this,因此就要看this指向哪里了。
由于咱们用了箭头函数,而箭头函数的this仍是指向父级的上下文,所以这里不用本身建立一个闭包,直接用this就能够:
init() {
$map.on('click',
event => this.handleMouseClick(event));
}复制代码
这种方式更加简单,第二种方法是使用ES5的bind函数绑定,以下代码:
init() {
$map.on('click',
this.handleMouseClick.bind(this));
}复制代码
这个bind看起来好像很神奇,但其实只要一行代码就能够实现一个bind函数:
Function.prototype.bind = function(context) {
return () => this.call(context);
}复制代码
就是返回一个函数,这个函数的this是指向的原始函数,而后让它call(context)绑定一下执行上下文就能够了。
柯里化就是函数和参数值结合产生一个新的函数,以下代码,假设有一个curry的函数:
function add(a, b) {
return a + b;
}
let add1 = add.curry(1);
console.log(add1(5)); // 6
console.log(add1(2)); // 3复制代码
怎么实现这样一个curry的函数?它的重点是要返回一个函数,这个函数有一些闭包的变量记录了建立时的默认参数,而后执行这个返回函数的时候,把新传进来的参数和默认参数拼一下变成完整参数列表去调本来的函数,因此有了如下代码:
Function.prototype.curry = function() {
let defaultArgs = arguments;
let that = this;
return function() {
return that.apply(this,
defaultArgs.concat(arguments)); }};
复制代码
可是因为参数不是一个数组,没有concat函数,因此须要把伪数组转成一个伪数组,能够用Array.prototype.slice:
Function.prototype.curry = function() {
let slice = Array.prototype.slice;
let defaultArgs = slice.call(arguments);
let that = this;
return function() {
return that.apply(this,
defaultArgs.concat(slice.call(arguments))); }};
复制代码
如今举一下柯里化一个有用的例子,当须要把一个数组降序排序的时候,须要这样写:
let data = [1,5,2,3,10];
data.sort((a, b) => b - a); // [10, 5, 3, 2, 1]复制代码
给sort传一个函数的参数,可是若是你的降序操做比较多,每次都写一个函数参数仍是有点烦的,所以能够用柯里化把这个参数固化起来:
Array.prototype.sortDescending =
Array.prototype.sort.curry((a, b) => b - a);
复制代码
这样就方便多了:
let data = [1,5,2,3,10];
data.sortDescending();
console.log(data); // [10, 5, 3, 2, 1]复制代码
有时候你可能怕你的对象被误改了,因此须要把它保护起来。
(1)Object.seal防止新增和删除属性
以下代码,当把一个对象seal以后,将不能添加和删除属性:
当使用严格模式将会抛异常:
(2)Object.freeze冻结对象
这个是不能改属性值,以下图所示:
同时可使用Object.isFrozen、Object.isSealed、Object.isExtensible判断当前对象的状态。
(3)defineProperty冻结单个属性
以下图所示,设置enumable/writable为false,那么这个属性将不可遍历和写:
怎么实现一个JS版的sleep函数?由于在C/C++/Java等语言是有sleep函数,可是JS没有。sleep函数的做用是让线程进入休眠,当到了指定时间后再从新唤起。你不能写个while循环而后不断地判断当前时间和开始时间的差值是否是到了指定时间了,由于这样会占用CPU,就不是休眠了。
这个实现比较简单,咱们可使用setTimeout + 回调:
function sleep(millionSeconds, callback) {
setTimeout(callback, millionSeconds);
}
// sleep 2秒
sleep(2000, () => console.log("sleep recover"));复制代码
可是使用回调让个人代码不可以和日常的代码同样像瀑布流同样写下来,我得搞一个回调函数看成参数传值。因而想到了Promise,如今用Promise改写一下:
function sleep(millionSeconds) {
return new Promise(resolve =>
setTimeout(resolve, millionSeconds));
}
sleep(2000).then(() => console.log("sleep recover"));复制代码
但好像仍是没有办法解决上面的问题,仍然须要传递一个函数参数。
虽然使用Promise本质上是同样的,可是它有一个resolve的参数,方便你告诉它何时异步结束,而后它就能够执行then了,特别是在回调比较复杂的时候,使用Promise仍是会更加的方便。
ES7新增了两个新的属性async/await用于处理的异步的状况,让异步代码的写法就像同步代码同样,以下async版本的sleep:
function sleep(millionSeconds) {
return new Promise(resolve =>
setTimeout(resolve, millionSeconds));
}
async function init() {
await sleep(2000);
console.log("sleep recover");
}
init();复制代码
相对于简单的Promise版本,sleep的实现仍是没变。不过在调用sleep的前面加一个await,这样只有sleep这个异步完成了,才会接着执行下面的代码。同时须要把代码逻辑包在一个async标记的函数里面,这个函数会返回一个Promise对象,当里面的异步都执行完了就能够then了:
init().then(() => console.log("init finished"));复制代码
ES7的新属性让咱们的代码更加地简洁优雅。
关于定时器还有一个很重要的话题,那就是setTimeout和setInterval的区别。以下图所示:
setTimeout是在当前执行单元都执行完才开始计时,而setInterval是在设定完计时器后就立马计时。能够用一个实际的例子作说明,这个例子我在《JS与多线程》这篇文章里面提到过,这里用代码实际地运行一下,以下代码所示:
let scriptBegin = Date.now();
fun1();
fun2();
// 须要执行20ms的工做单元
function act(functionName) {
console.log(functionName, Date.now() - scriptBegin);
let begin = Date.now();
while(Date.now() - begin < 20);
}
function fun1() {
let fun3 = () => act("fun3");
setTimeout(fun3, 0);
act("fun1");
}
function fun2() {
act("fun2 - 1");
var fun4 = () => act("fun4");
setInterval(fun4, 20);
act("fun2 - 2");
}
复制代码
这个代码的执行模型是这样的:
控制台输出:
与上面的模型分析一致。
接着再讨论最后一个话题,函数节流
节流的目的是为了避免想触发执行得太快,如:
咱们先看一下,resize/mousemove事件1s种能触发多少次,因而写了如下驱动代码:
let begin = 0;
let count = 0;
window.onresize = function() {
count++;
let now = Date.now();
if (!begin) {
begin = now;
return;
}
if((now - begin) % 3000 < 60) {
console.log(now - begin,
count / (now - begin) * 1000);
}
};复制代码
当把窗口拉得比较快的时候,resize事件大概是1s触发40次:
须要注意的是,并非说你拉得越快,触发得就越快。实际状况是,拉得越快触发得越慢,由于拉动的时候页面须要重绘,变化得越快,重绘的次数也就越多,因此致使触发得更少了。
mousemove事件在个人电脑的Chrome上1s大概触发60次:
若是你须要监听resize事件作DOM调整的话,这个调整比较费时,1s要调整40次,这样可能会响应不过来,而且不须要调整得这么频繁,因此要节流。
怎么实现一个节流呢,书里是这么实现的:
function throttle(method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function() {
method.call(context);
}, 100);
}复制代码
每次执行都要setTimeout一下,若是触发得很快就把上一次的setTimeout清掉从新setTimeout,这样就不会执行很快了。可是这样有个问题,就是这个回调函数可能永远不会执行,由于它一直在触发,一直在清掉tId,这样就有点尴尬,上面代码的本意应该是100ms内最多触发一次,而实际状况是可能永远不会执行。这种实现应该叫防抖,不是节流。
把上面的代码稍微改造一下:
function throttle(method, context) {
if (method.tId) {
return;
}
method.tId = setTimeout(function() {
method.call(context);
method.tId = 0;
}, 100);
}
复制代码
这个实现就是正确的,每100ms最多执行一次回调,原理是在setTimeout里面把tId给置成0,这样能让下一次的触发执行。实际实验一下:
大概每100ms就执行一次,这样就达到咱们的目的。
可是这样有一个小问题,就是每次执行都是要延迟100ms,有时候用户可能就是最大化了窗口,只触发了一次resize事件,可是此次仍是得延迟100ms才能执行,假设你的时间是500ms,那就得延迟半秒,所以这个实现不太理想。
须要优化,以下代码所示:
function throttle(method, context) {
// 若是是第一次触发,马上执行
if (typeof method.tId === "undefined") {
method.call(context);
}
if (method.tId) {
return;
}
method.tId = setTimeout(function() {
method.call(context);
method.tId = 0;
}, 100);
}复制代码
先判断是否为第一次触发,若是是的话马上执行。这样就解决了上面提到的问题,可是这个实现仍是有问题,由于它只是全局的第一次,用户最大化以后,隔了一会又取消最大化了就又有延迟了,而且第一次触发会执行两次。那怎么办呢?
笔者想到了一个方法:
function throttle(method, context) {
if (!method.tId) {
method.call(context);
method.tId = 1;
setTimeout(() => method.tId = 0, 100);
}
}复制代码
每次触发的时候马上执行,而后再设定一个计时器,把tId置成0,实际的效果以下:
这个实现比以前的实现还要简洁,而且可以解决延迟的问题。
因此经过节流,把执行次数降到了1s执行10次,节流时间也能够控制,但同时失去了灵敏度,若是你须要高灵敏度就不该该使用节流,例如作一个拖拽的应用。若是拖拽节流了会怎么样?用户会发现拖起来一卡一卡的。
笔者从新看了高程的《高级技巧》的章节结合本身的理解和实践总结了这么一篇文章,个人体会是若是看书看博客只是看成睡前读物看一看其实收获不是很大,没有实际地把书里的代码实践一下,没有结合本身的编码经验,就不能用本身的理解去融入这个知识点,从而转化为本身的知识。你可能会说我看了以后就会印象啊,有印象仍是好的,可是你花了那么多时间看了那本书只是获得了一个印象,你本身都没有实践过的印象,这个印象又有多靠谱呢。若是别人问到了这个印象,你可能会回答出一些连不起来的碎片,就会给人一种背书的感受。还有有时候书里可能会有一些错误或者过期的东西,只有实践了才能出真知。