JS高级技巧

作者 : 开心源码 本文共8904个字,预计阅读时间需要23分钟 发布时间: 2022-05-11 共60人阅读

1. 安全的类型检测

这个问题是怎样安全地检测一个变量的类型,例如判断一个变量能否为一个数组。通常的做法是用instanceof,如下代码所示:

let data = [1, 2, 3]; console.log(data instanceof Array); //true

但是上面的判断在肯定条件下会失败——就是在iframe里面判断一个父窗口的变量的时候。写个demo验证一下,如下主页面的main.html:

 

在iframe.html判断一下父窗口的变量类型:

在iframe里面用window.parent得到父窗口的全局window对象,这个不论跨不跨域都没有问题,进而能得到父窗口的变量,而后使用instanceof判断。最后运行结果如下:

JS高级技巧

能看到父窗口的判断是正确的,而子窗口的判断是false,因而一个变量明明是Array,但却不是Array,这是为什么呢?既然这个是父子窗口才会有的问题,于是试一下把Array改成父窗口的Array,即window.parent.Array,如下图所示:

JS高级技巧

这次返回了true,而后再变换一下其它的判断,如上图,最后能知道根本起因是上图最后一个判断:

Array !== window.parent.Array

它们分别是两个函数,父窗口定义了一个,子窗口又定义了一个,内存地址不一样,内存地址不一样的Object等式判断不成立,而window.parent.arrayData.constructor返回的是父窗口的Array,比较的时候是在子窗口,用的是子窗口的Array,这两个Array不相等,所以导致判断不成立。

那怎样办呢?

因为不可以用Object的内存地址判断,能用字符串的方式,由于字符串是基本类型,字符串比较只需每个字符都相等就好了。ES5提供了这么一个方法Object.prototype.toString,我们先小试牛刀,试一下不同变量的返回值:

JS高级技巧

能看到假如是数组返回”[object Array]”,ES5对这个函数是这么规定的:

JS高级技巧

也就是说这个函数的返回值是“[object ”开头,后面带上变量类型的名称和右括号。因而既然它是一个标准语法规范,所以能使用这个函数安全地判断变量是不是数组。

能这么写:

Object.prototype.toString.call([1, 2, 3]) === "[object Array]"

注意要用call,而不是直接调使用,call的第一个参数是context执行上下文,把数组传给它作为执行上下文。

有一个比较有趣的现象是ES6的class也是返回function:

JS高级技巧

所以能知道class也是使用function实现的原型,也就是说class和function本质上是一样的,只是写法上不一样。

那是不是说不可以再用instanceof判断变量类型了?不是的,当你需要检测父页面的变量类型就得用这种方法,本页面的变量还是能用instanceof或者者constructor的方法判断,只需你可以确保这个变量不会跨页面。由于对于大多数人来说,很少会写iframe的代码,所以没有必要搞一个比较麻烦的方式,还是使用简单的方式就好了。

2. 惰性载入函数

有时候需要在代码里面做少量兼容性判断,或者者是做少量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的判断,但如同不是特别实惠,毕竟为了减少一次判断,引入了一个惰性函数的概念,所以你可可以要权衡一下这种引入能否值得,假如有三五个判断应该还是比较好的。

3. 函数绑定

有时候要把一个函数当作参数传递给另一个函数执行,此时函数的执行上下文往往会发生变化,如下代码:

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)绑定一下执行上下文即可以了。

4. 柯里化

柯里化就是函数和参数值结合产生一个新的函数,如下代码,假设有一个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]

5. 防止篡改对象

有时候你可可以怕你的对象被误改了,所以需要把它保护起来。

(1)?Object.seal防止新添加和删除属性

如下代码,当把一个对象seal之后,将不可以增加和删除属性:

当用严格模式将会抛异常:

(2)Object.freeze冻结对象

这个是不可以改属性值,如下图所示:

同时能用Object.isFrozen、Object.isSealed、Object.isExtensible判断当前对象的状态。

(3)defineProperty冻结单个属性

如下图所示,设置enumable/writable为false,那么这个属性将不可遍历和写:

6. 定时器

怎样实现一个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  act("fun3"); setTimeout(fun3, 0); act("fun1"); } function fun2() { act("fun2 - 1"); var fun4 = () => act("fun4"); setInterval(fun4, 20); act("fun2 - 2"); }

这个代码的执行模型是这样的:

控制台输出:

与上面的模型分析一致。

接着再探讨最后一个话题,函数节流

7. 函数节流throttling

节流的目的是为了不想触发执行得太快,如:

  • ?监听input触发搜索

  • ?监听resize做响应式调整

  • ?监听mousemove调整位置

我们先看一下,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,实际的效果如下:

JS高级技巧

这个实现比之前的实现还要简洁,并且可以够处理推迟的问题。

?所以通过节流,把执行次数降到了1s执行10次,节流时间也能控制,但同时失去了灵敏度,假如你需要高灵敏度就不应该用节流,例如做一个拖拽的应使用。假如拖拽节流了会怎样样?使用户会发现拖起来一卡一卡的

作者:人人网FED

链接:https://juejin.im/post/59ab7b36f265da24934b2470

来源:掘金

著作权归作者所有。商业转载请联络作者取得受权,非商业转载请注明出处。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » JS高级技巧

发表回复