ホームページ > 記事 > ウェブフロントエンド > 知っておくべき高度な JS スキル (概要)
前回の記事「チュートリアル:JSとAPIを使って天気Webアプリを作る方法(集)」では、JSとAPIを使って天気を作る方法を紹介しました。 Web アプリケーションのメソッド。以下の記事では、高度な JS スキルを紹介します。ある程度の参考価値があるので、困っている友人は参考にしてください。お役に立てれば幸いです。
この記事は、『JS 上級プログラミング』の第 23 章「高度なテクニック」に基づいた読み物共有です。この記事は、本の考え方に従い、私自身の理解と経験に基づいて拡張し、本のいくつかの問題点を指摘します。安全な型チェック、関数の遅延ロード、フリーズされたオブジェクト、タイマーなどのトピックについて説明します。
この質問は、変数が配列であるかどうかを判断するなど、変数の型を安全に検出する方法です。通常のアプローチは、次のコードに示すように、instanceof
を使用することです:
let data = [1, 2, 3]; console.log(data instanceof Array); //true
ただし、上記の判断は特定の条件下、つまり iframe での親の判断下では失敗します。
ウィンドウ変数の場合。メイン ページの main.html
のように、検証する demo
を作成します。
<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.Arrayこれらは 2 つの関数であり、1 つは親ウィンドウに が定義されており、子ウィンドウに別のものが定義されています。メモリ アドレスが異なります。メモリ アドレスが異なる
Object 式は成立せず、
window.parent.arrayData.constructor は、親ウィンドウの
Array を返します。比較はサブウィンドウで行われ、サブウィンドウの
Array が使用されます。2 つの
Array は等しくないため、判定は無効です。
Object のメモリ アドレスでは判断できないため、文字列メソッドを使用できます。文字列は基本型であり、文字列比較では各文字が等しいことのみが必要です。
ES5 は、そのようなメソッド
Object.prototype.toString を提供します。まずそれを試して、さまざまな変数の戻り値を試してみましょう:
配列が「[オブジェクト配列]」を返す場合、ES5 はこの関数を次のように規定していることがわかります: ES5 関数アドレス: https://262.ecma-international .org /5.1/#sec-15.2.4.2つまり、この関数の戻り値は "
[object##" で始まります。 #" の後に変数タイプの名前と閉じ括弧が続きます。したがって、これは標準の構文仕様であるため、この関数を使用して変数が配列であるかどうかを安全に判断できます。 は次のように記述できます:
Object.prototype.toString.call([1, 2, 3]) === "[object Array]"
直接呼び出すのではなく、
call を使用するように注意してください。call
の最初のパラメータは ## です。 #context 実行コンテキスト。配列を実行コンテキストとして渡します。
興味深い現象は、
ES6
class も
function:
class も
function で実装されたプロトタイプであることがわかります。つまり、class
と function
は本質的に同じです。書き方が違います。 <p>那是不是说不能再使用<code>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"); }
这个代码的执行模型是这样的:
控制台输出:
与上面的模型分析一致。
接着再讨论最后一个话题,函数节流
节流的目的是为了不想触发执行得太快,如:
监听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,实际的效果如下:
这个实现比之前的实现还要简洁,并且能够解决延迟的问题。
所以通过节流,把执行次数降到了1s
执行10次,节流时间也可以控制,但同时失去了灵敏度,如果你需要高灵敏度就不应该使用节流,例如做一个拖拽的应用。如果拖拽节流了会怎么样?用户会发现拖起来一卡一卡的。
笔者重新看了高程的《高级技巧》的章节结合自己的理解和实践总结了这么一篇文章,我的体会是如果看书看博客只是当作睡前读物看一看其实收获不是很大,没有实际地把书里的代码实践一下,没有结合自己的编码经验,就不能用自己的理解去融入这个知识点,从而转化为自己的知识。你可能会说我看了之后就会印象啊,有印象还是好的,但是你花了那么多时间看了那本书只是得到了一个印象,你自己都没有实践过的印象,这个印象又有多靠谱呢。如果别人问到了这个印象,你可能会回答出一些连不起来的碎片,就会给人一种背书的感觉。还有有时候书里可能会有一些错误或者过时的东西,只有实践了才能出真知。
推荐学习:JS视频教程
以上が知っておくべき高度な JS スキル (概要)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。