blog
blog copied to clipboard
JavaScript问题集锦(二)
不知不觉,从事前端 4 年多了,距离本篇的前作 JavaScript问题集锦 也有 2 年多了 —— 青葱岁月啊。
又忽然想起“我变秃了,也变强了!”的梗,正好程序员工作久了,容易变秃... 当然我没有秃,所以说 😂
收起半夜突然来的感概,正式解释下文章题目和目的: 本文还是会以 JS 的一个个知识点为粒度来讲,这算是对之前那篇的继承。这个秋天,静极思动,面了BA以及其它一些公司,和面试官的尬聊中,有些觉得很懂的东西并没有解释的很好,这里也提醒下自己和大家:学无止境。
1. 从 IIFE 说一说 Expression 和 Statement
IIFE (Immediately Invoked Function Expression) 即立即调用/执行函数表达式。我们常看到(包括某些库中):
(function() {})()
上面的即 IIFE 的一种写法,匿名函数会立即执行。下面是一些等价写法:
(function() {}())
!function() {}()
~function() {}()
是不是觉得很熟悉,然后觉得没什么要注意的?那下面问个问题:
function(){}()
它是 IIFE 吗?为什么?Console 中输入会发生什么?单独拎出来这样问是不是有些发懵?
上面的代码运行的话会报错,并且更进一步,单独执行 function(){}
也会报错:

下面首先简要解释下原因:
JS 应用是由(无语法错误的) statements 组成的。
当我们单独输入:function (){}
时,解释器其实期待的是合法的 statement,即一个函数声明。但很抱歉,函数声明必须有 name,所以这里报错了。
同理,function (){}()
是一样的错误原因,因为当解释器首先看到关键字 function 时,它就认为要接收一个函数声明了,但我们并没有满足这个规则。
下面的图可以帮助理解:

接下来我们更深入一点,来全面了解下 JS 中的 Statements 和 Expression。
An expression is any valid unit of code that resolves to a value.
对 expression ,一句话:任意合法的产生值的代码单元都是表达式。所以:
-
5
-
this
-
a = 5
-
func()
-
(function () {})
等等都是表达式。更详细的可参考 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators。
对于 statement,我们可以直接看规范 http://es5.github.io/#x12,可以看到 statement 有
- VariableStatement (变量定义语句)
- ExpressionStatement (表达式语句)
- ...
等等。其中,我们重点要讲一讲 ExpressionStatement (表达式语句) ,这是我们这个问题的由来。
ExpressionStatement :
[lookahead ∉ {{, function}] Expression ;
表达式语句的定义如上,用中文解释下就是 合法的 expression 加上 ;
(即 expression;
)就是表达式语句了。但是,
-
{
开头的不是,因为会和 BlockStatement 产生歧义 -
function
开头的不是,因为会和 FunctionDeclaration 产生歧义
所以你看,一切写在规范里了。
结合规范,我们就知道开头如果是 function
,那么一律按函数定义来解析,不合法就报错;而我们可以通过 ()
括号/group操作符来避免。
同样{a:1}.a
报错就是因为开头是{
被当作 BlockStatement 解释了,想当作对象那加括号吧:({a:1}).a
。
讲得很清楚,手动点赞! 👍
2. 关于 String.fromCharCode
的一点讨论
看规范 ,我们可以知道:
String.fromCharCode ( [ char0 [ , char1 [ , … ] ] ] )
可以接收多个参数,并返回同样多个字符。
比如:
String.fromCharCode(50, 51, 52) // 234
但是,我们可以看一个反例(浏览器未严格遵循规范的):
String.fromCharCode(55297, 56375) // '𐐷'
很有意思对不对?两个参数却只返回一个字符。
当然这是我查阅怎么从 utf16
编码读取字符串时发现的,具体原因也和编码有关,~~这里先记一下,之后给出详细解答。~~
https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae 讲得很透彻 ,关心这个知识点的可以自行阅读。下面是一张可能会吸引你去读这篇文章的截图:

详细解读见 #4
3. <script>
标签与async
和 defer
属性
浏览器在解析 HTML 时碰到普通的 <script>
标签,会暂停解析,下载并执行完脚本后,再重新开始解析。我想这个大多数人应该都了解,但async
和 defer
属性有什么区别,大家可能会有疑惑。
如上图所示:
1、碰到async(<script async src="app.js"></script>
)脚本,浏览器下载脚本的同时继续解析HTML,下载完成后,浏览器暂停解析HTML并执行脚本;
2、碰到defer(<script defer src="app.js"></script>
)脚本,浏览器下载脚本的同时继续解析HTML,在HTML解析完成后按序执行脚本;更明确一点,脚本在domInteractive
后,在DOMContentLoaded
前执行。
3、async不保证各脚本的执行顺序而 defer 保证按序执行。
4. <input>
和清除按钮 X
的显示问题
假设这样一个场景,有一个输入框 <input>
与一个清除按钮 X
:
- 输入框focus并且有文字时清除按钮显示;(输入框blur时清除按钮隐藏)
- 点击清除按钮,输入框文字清空。
看起来实现没什么难的,我也不是要问清除按钮怎么用纯CSS来写,问题是,按钮的 click
和输入框的 blur
事件的发生顺序?
blur
先于 click
发生,那么问题来了,blur时清除按钮就被移除,导致 click 事件没触发,也就导致文字不会被清除。
临时方案1 :blur 的回调塞到 setTimeout(fn, 0) 里,让清除按钮的操作延后,是不是就可以让 click 触发?
- 手机浏览器测试可以;
- PC 上浏览器测试失败,当setTimeout 到十几毫秒时有概率成功。
不是个可靠方案,放弃。
临时方案2 :不移除按钮DOM,改为设置透明度 0。这样 click 百分百可以触发,但需要考虑透明度 0 点击的时候当作无效点击。
不是完美方案。
方案3 :利用 onmousedown
的 event.preventDefault()
来让 blur
在 click
之后发生。
按钮的 onmousedown
优先于输入框的 blur 发生,在 onmousedown
中 preventDefault
即可避免 blur 发生在 click 之前。
详情见 https://stackoverflow.com/questions/17769005/onclick-and-onblur-ordering-issue。
完美。
5. 当出现垂直滚动条之后,为什么出现了水平滚动条?【css、layout、vw】
在测试UI的时候,发现个很有意思的问题:UI一直正常,直到浏览器宽度增加到某个值,出现了水平滚动条。
怀疑是某个元素布局问题,查找,并没找出原因。
用排除法,逐一删除相比线上不同的元素,删除第一个时,UI已经恢复正常,但是反复检查这个元素,并没有什么不对的地方;换第二个删除,UI也恢复正常....
最终找到原因,删除一个元素时,垂直滚动条消失!滚动条的原因!
继续查找原因才知道:宽度单位 vw
是包括滚动条宽度的,即100vw
的宽度是 document.documentElement.offsetWidth
,使用 rem 布局时需要额外注意。
html {
font-size: calc(100vw / 3.75);
}
解决办法:
::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
隐藏滚动条即可。