一 目录
不折腾的前端,和咸鱼有什么区别
二 前言
返回目录
在 JavaScript 复习过程中,可能会碰到:
null
和undefined
的区别?addEventListener
函数?
这样杂七杂八的问题,亦或者 a == 1 && a == 2
这样有趣的问题。
将它们归类到 JavaScript 基础,并在本篇文章中一一讲述。
同时,会有十几道简单题目练手。
三 DOM 常用 API
返回目录
可以使用 document
或 window
元素的 API 来操作文档本身或获取文档的子类(Web 页面中的各种元素)。
// 获取元素
const node = document.getElementById(id); // 或者 querySelector(".class|#id|name");
// 创建元素
const heading = document.createElement(name); // name: p、div、h1...
heading.innerHTML = '';
// 添加元素
document.body.appendChild(heading);
// 删除元素
document.body.removeChild(node);
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>DOM 操作</title>
<style>
div {
border: 1px solid #ccc;
padding: 50px;
width: 100px;
}
</style>
</head>
<body>
<div id="dom1">元素 1</div>
<div class="dom2">元素 2</div>
<button class="btn">点我</button>
<script>
(function() {
const btn = document.querySelector('.btn');
// 注册点击事件
btn.onclick = function() {
const dom1 = document.getElementById('dom1');
// 第一种添加元素
const newDom1 = document.createElement('p');
newDom1.innerHTML = '<a href="https://github.com/LiangJunrong/document-library">jsliang 的文档库</a>';
dom1.appendChild(newDom1);
// 第二种添加元素
const newDom2 = document.createElement('ul');
newDom2.innerHTML = `
<li>aaa</li>
<li>bbb</li>
`;
document.body.appendChild(newDom2);
// 移除元素
const dom2 = document.querySelector('.dom2');
document.body.removeChild(dom2);
}
})()
</script>
</body>
</html>
四 null 和 undefined 的区别
返回目录
null
表示无
的对象,也就是此处不应该有值;而undefined
表示未定义。- 在转换数字的时候,
Number(null)
为0
,而Number(undefined)
为NaN
。
使用场景细分如下:
null
:
- 作为函数的参数,表示该函数的参数不是对象。
- 作为对象原型链的终点。
Object.prototype.__proto__ === null
undefined
:
- 变量被声明但是没有赋值,等于
undefined
。 - 调用函数时,对应的参数没有提供,也是
undefined
。 - 对象没有赋值,这个属性的值为
undefined
。 - 函数没有返回值,默认返回
undefined
。
五 事件流
返回目录
什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM 2
级事件流包括下面几个阶段。
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
如何让事件先冒泡后捕获:
在 DOM
标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。
5.1 addEventListener
返回目录
addEventListener
方法将指定的监听器注册到 EventTarget
上,当该对象触发指定的事件时,指定的回调函数就会被执行。
addEventListener
事件目标可以是文档上的元素 Element
、Document
和 Window
或者任何其他支持事件的对象(例如 XMLHttpRequest
)。
参考文档:EventTarget.addEventListener - MDN
- 语法:
target.addEventListener(type, listener, options/useCapture)
type
:表示监听事件类型的字符串listener
:所监听的事件触发,会接受一个事件通知对象。options
:一个指定有关listener
属性的可选参数对象。可选值有capture
(事件捕获阶段传播到这里触发)、once
(在listener
添加之后最多值调用一次)、passive
(设置为true
时表示listener
永远不会调用preventDefault()
)。useCapture
:在 DOM 树中,注册了listener
的元素,是否要先于它下面的EventTarget
调用该listener
。
addEventListener
的第三个参数涉及到冒泡和捕获,为true
时是捕获,为false
时是冒泡。或者是一个对象
{ passive: true }
,针对的是Safari
浏览器,禁止/开启使用滚动的时候要用到
- 示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>监听器</title>
</head>
<body>
<table id="outside">
<tr><td id="t1">one</td></tr>
<tr><td id="t2">two</td></tr>
</table>
<script>
(function() {
// 添加函数
const modifyText = (text) => {
const t2 = document.querySelector('#t2');
if (t2.firstChild.nodeValue === text) {
t2.firstChild.nodeValue = 'two';
} else {
t2.firstChild.nodeValue = text;
}
}
// 给 Table 添加事件监听器
const element = document.querySelector('#outside');
element.addEventListener('click', function() { modifyText('four') }, false);
})()
</script>
</body>
</html>
如上,这个示例简单实现了点击 two
切换到 four
,点击 four
再切换到 two
的效果。
5.2 原理
返回目录
事件捕获和事件冒泡分别是 网景(Netscape)和 IE 对 DOM
事件产生顺序的描述。
网景 认为 DOM
接收的事件应该最先是 window
,然后到 document
,接着一层一层往下,最后才到具体的元素接收到事件,即 事件捕获。
IE 则认为 DOM
事件应该是具体元素先接收到,然后再一层一层往上,接着到 document
,最后才到 window
,即 事件冒泡。
最后 W3C 对这两种方案进行了统一:将 DOM
事件分为两个阶段,事件捕获和事件冒泡阶段。
当一个元素被点击,首先是事件捕获阶段,window
最先接收事件,然后一层一层往下捕获,最后由具体元素接收;之后再由具体元素再一层一层往上冒泡,到 window
接收事件。
所以:
- 事件冒泡:当给某个目标元素绑定了事件之后,这个事件会依次在它的父级元素中被触发(当然前提是这个父级元素也有这个同名称的事件,比如子元素和父元素都绑定了
click
事件就触发父元素的click
)。 - 事件捕获:和冒泡相反,会从上层传递到下层。
5.3 案例
返回目录
结合自定义事件耍个例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>自定义事件</title>
</head>
<body>
<ul class="ul">
<li class="li">
<button class="btn">点我</button>
</li>
</ul>
<script>
window.onload = function() {
const myEvent = document.createEvent('CustomEvent');
myEvent.initEvent('myEvent', true, true);
const btn = document.querySelector('.btn');
btn.addEventListener('myEvent', function(e) {
console.log('button');
})
const li = document.querySelector('.li');
li.addEventListener('myEvent', (e) => {
console.log('li');
})
const ul = document.querySelector('.ul');
li.addEventListener('myEvent', (e) => {
console.log('ul');
})
document.addEventListener('myEvent', (e) => {
console.log('document');
})
window.addEventListener('myEvent', (e) => {
console.log('window');
})
setTimeout(() => {
btn.dispatchEvent(myEvent);
}, 2000);
};
</script>
</body>
</html>
Chrome 输出下顺序是:button
-> li
-> ul
-> document
-> window
如果是捕获的话,那么则相反。
5.4 练习题
返回目录
点击一个 input
依次触发的事件
const text = document.getElementById('text');
text.onclick = function (e) {
console.log('onclick')
}
text.onfocus = function (e) {
console.log('onfocus')
}
text.onmousedown = function (e) {
console.log('onmousedown')
}
text.onmouseenter = function (e) {
console.log('onmouseenter')
}
正确顺序是:onmouseenter -> onmousedown -> onfocus -> onclick
。
如果加上 onmouseup
,那就是:
onmouseenter -> onmousedown -> onfocus -> onmouseup -> onclick
5.5 阻止冒泡
返回目录
event.stopPropagation();
btn.addEventListener('myEvent', function(e) {
console.log('button');
event.stopPropagation();
})
通过阻止冒泡,程序只会输出 button
,而不会继续输出 li
等。
5.6 onmouseover 和 onmouseenter 区别
返回目录
这两者都是移入的时候触发,但是 onmouseover
会触发多次,而 onmouseenter
只在进去的时候才触发。
5.7 科普
返回目录
并不是所有的事件都有冒泡,例如:
onblur
onfocus
onmouseenter
onmouseleave
六 typeof 和 instanceof 的区别
返回目录
typeof
:对某个变量类型的检测,基本类型除了null
之外,都能正常地显示为对应的类型,引用类型除了函数会显示为function
,其他都显示为object
。instanceof
主要用于检测某个构造函数的原型对象在不在某个对象的原型链上。
typeof
会对 null
显示错误是个历史 Bug,typeof null
输出的是 object
,因为 JavaScript 早起版本是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000
开头代表是对象然而 null
表示为全零,所以它错误判断为 object
。
另外还有 Object.prototype.toString.call()
进行变量判断。
详细可见:JavaScript - 变量
七 一句话描述 this
返回目录
对于函数而言,指向最后调用函数的那个对象,是函数运行时内部自动生成的一个内部对象,只能在函数内部使用;对于全局而言,this
指向 window
。
八 JS 位置
返回目录
clientHeight
:表示可视区域的高度,不包含border
和滚动条offsetHeight
:表示可视区域的高度,包含了border
和滚动条scrollHeight
:表示了所有区域的高度,包含了因为滚动被隐藏的部分clientTop
:表示边框border
的厚度,在未指定的情况下一般为0
scrollTop
:滚动后被隐藏的高度,获取对象相对于由offsetParent
属性指定的父坐标(CSS 定位的元素或body
元素)距离顶端的高度。
九 JS 拖拽
返回目录
- 通过
mousedown
、mousemove
、mouseup
方法实现 - 通过 HTML5 的
Drag
和Drop
实现
十 setTimeout 实现 setInterval
返回目录
这算另类知识点吧,本来打算归类手写源码系列的,但是想想太 low
了,没牌面,入基础系列吧:
const say = () => {
// do something
setTimeout(say, 200);
};
setTimeout(say, 200);
清除这个定时器:
let i = 0;
const timeList = [];
const say = () => {
// do something
console.log(i++);
timeList.push(setTimeout(say, 200));
};
setTimeout(say, 200);
setTimeout(() => {
for (let i = 0; i < timeList.length; i++) {
clearTimeout(timeList[i]);
}
}, 1000);
十一 实现 Sleep
返回目录
如下,实现 1000
毫秒后执行其他内容:
const sleep = time => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time);
}, time);
});
};
sleep(1000).then((res) => {
console.log(res);
});
十二 执行上下文
返回目录
12.1 执行上下文类型
返回目录
JavaScript 中有 3 种执行上下文类型:
- 全局执行上下文:这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的
window
对象(浏览器的情况下),并且设置this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 - 函数执行上下文:每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
- Eval 函数执行上下文:执行在
eval
函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用eval
,所以在这里我不会讨论它。
12.2 执行栈
返回目录
执行栈,也就是在其它编程语言中所说的 “调用栈”,是一种拥有 LIFO
(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
十三 函数式编程
返回目录
函数式编程(Functional Programming,简称 FP)。
函数式编程:通过对面向对象式编程代码的拆分,将各个功能独立出来,从而达到功能独立、易复用等目的。
举例:代码转换
['john-reese', 'harold-finch', 'sameen-shaw']
// 转换成
[{name: 'John Reese'}, {name: 'Harold Finch'}, {name: 'Sameen Shaw'}]
对上面代码进行转换。
const arr = ['john-reese', 'harold-finch', 'sameen-shaw'];
const newArr = [];
for (let i = 0, len = arr.length; i < len ; i++) {
let name = arr[i];
let names = name.split('-');
let newName = [];
for (let j = 0, naemLen = names.length; j < naemLen; j++) {
let nameItem = names[j][0].toUpperCase() + names[j].slice(1);
newName.push(nameItem);
}
newArr.push({ name : newName.join(' ') });
}
return newArr;
这份代码中,有 2 个部分:
- 拆分数组中字符串,将字符串变成人名。
john-reese -> John Reese
- 将数组转换成对象。
['John Reese'] -> [{ name: 'John Reese' }]
所以我们直接可以改动:
/**
* @name 改变人名展示方式
* @param {array} arr 需要改变的数组
* @param {string} type 支持不同格式的人名
*/
const changeName = (arr, type) => {
return arr.map(item => item.split(type).map(name => name[0].toUpperCase() + name.slice(1)).join(' '));
};
/**
* @name 数组改变成对象
* @param {array} arr 需要改变的数组
* @param {string} key 对应变成什么字段
* @return {object} 返回改变后的对象
*/
const arrToObj = (arr, key) => {
return arr.map(item => ({ [key]: item }));
};
const result = arrToObj(changeName(['john-reese', 'harold-finch', 'sameen-shaw'], '-'), 'name');
console.log(result); // [ { name: 'John Reese' }, { name: 'Harold Finch' }, { name: 'Sameen Shaw' } ]
嗨,这不就是对功能封装吗?一般来说工作中出现 2 次以上的代码才进行封装。
函数式编程就是对可以抽离的功能都进行抽取封装。
到这里仿佛掌握了真理,jsliang 也没详细了解定义撒,希望没误导。
13.1 函数式编程特点
返回目录
- 函数是一等公民。可以利用这点让它支持抽取到外部。
- 声明做某件时间。函数式编程大多数声明某个函数需要做什么,而不是它怎么做的。
- 便于垃圾回收。函数内部的变量方便垃圾回收,不会产生太多的变量,用户不需要大量的定义。
- 数据不可变。函数式编程要求所有的数据都是不可变的,如果需要修改某个对象,应该新建后再修改,而不是污染原本的数据。
- 无状态。不管什么时候运行,同一个函数对相同的输入返回相同的输出,而不依赖外部状态的变化。
- 无副作用。功能 A 应该仅仅为了完成它的实现,而不会随着外部的改变而改变,这样当它执行完毕之后,就可以将其内部数据进行回收。并且它不会修改传入的参数。
注重引用值(Object、Array)的传递,尽可能不要污染传入的数据。
13.2 纯函数
返回目录
纯函数的概念有 2 点:
- 不依赖外部状态(无状态):函数的运行结果不依赖全局变量,
this
指针,IO
操作等。 - 没有副作用(数据不变):不修改全局变量,不修改入参。
优点:
- 便于测试和优化
- 可缓存性
- 自文档化
- 更少 Bug
十四 渐进式网络应用(PWA)
返回目录
渐进式网络应用(PWA)是谷歌在 2015 年底提出的概念。基本上算是 Web 应用程序,但在外观和感觉上与原生 App 类似。支持 PWA 的网站可以提供脱机工作、推送通知和设备硬件访问等功能。
14.1 优点
返回目录
- 更小更快: 渐进式的 Web 应用程序比原生应用程序小得多。他们甚至不需要安装。这是他们没有浪费磁盘空间和加载速度非常快。
- 响应式界面: PWA 支持的网页能够自动适应各种屏幕大小。它可以是手机、平板、台式机或笔记本。
- 无需更新: 大多数移动应用程序需要每周定期更新。与普通网站一样,每当用户交互发生且不需要应用程序或游戏商店批准时,PWA 总是加载最新更新版本。
- 高性价比:原生移动应用需要分别为 Android 和 iOS 设备开发,开发成本非常高。另一方面,PWA 有着相同的功能,但只是先前价格的一小部分,开发成本低。
- SEO 优势:搜索引擎可以发现 PWA,并且加载速度非常快。就像其他网站一样,它们的链接也可以共享。提供良好的用户体验和结果,在 SEO 排名提高。
- 脱机功能:由于 Service Worker API 的支持,可以在脱机或低internet连接中访问PWAs。
- 安全性:PWA 通过 HTTPS 连接传递,并在每次交互中保护用户数据。
- 推送通知:通过推送通知的支持,PWA 轻松地与用户进行交互,提供非常棒的用户体验。
- 绕过应用商店:原生 App 如果需要任何新的更新,需要应用商店几天的审批,且有被拒绝或禁止的可能性,对于这方面来说,PWA 有它独特的优势,不需要 App Store 支持。更新版本可以直接从 Web 服务器加载,无需 App Store 批准。
- 零安装:在浏览过程中,PWA 会在手机和平板电脑上有自己的图标,就像移动应用程序一样,但不需要经过冗长的安装过程。
14.2 缺点
返回目录
- 对系统功能的访问权限较低:目前 PWA 对本机系统功能的访问权限比原生 App 有限。而且,所有的浏览器都不支持它的全部功能,但可能在不久的将来,它将成为新的开发标准。
- 多数 Android,少数 iOS:目前更多的支持来自 Android。iOS 系统只提供了部分。
- 没有审查标准:PWA 不需要任何适用于应用商店中本机应用的审查,这可能会加快进程,但缺乏从应用程序商店中获取推广效益。
十五 规范化
返回目录
CommonJS
规范、AMD
规范、CMD
规范、ES6 Modules
规范这 4 者都是前端规范化的内容,那么它们之间区别是啥呢?
在没有这些之前,我们通过:
- 一个函数就是一个模块。
function fn() {}
- 一个对象就是一个模块。
let obj = new Object({ ... })
- 立即执行函数(IIFE)。
(function() {})()
15.1 CommonJS 规范
返回目录
这之后,就有了 CommonJS
规范,其实 CommonJS
我们见得不少,就是 Node
的那套:
- 导出:
module.exports = {}
、exports.xxx = 'xxx'
- 导入:
require(./index.js)
- 查找方式:查找当前目录是否具有文件,没有则查找当前目录的
node_modules
文件。再没有,冒泡查询,一直往系统中的npm
目录查找。
它的特点:
- 所有代码在模块作用域内运行,不会污染其他文件
require
得到的值是值的拷贝,即你引用其他 JS 文件的变量,修改操作了也不会影响其他文件
它也有自己的缺陷:
- 应用层面。在
index.html
中做var index = require('./index.js')
操作报错,因为它最终是后台执行的,只能是index.js
引用index2.js
这种方式。 - 同步加载问题。
CommonJS
规范中模块是同步加载的,即在index.js
中加载index2.js
,如果index2.js
卡住了,那就要等很久。
15.2 AMD 规范
返回目录
为什么有 AMD
规范?
答:CommonJS
规范不中用:
- 适用客户端
- 等待加载(同步加载问题)。
所以它做了啥?
可以采用异步方式加载模块。AMD
是 Asynchronous Module Definition
的缩写,也就是 “异步模块定义”,记住这个 async
就知道它是异步的了。
15.3 CMD 规范
返回目录
CMD (Common Module Definition), 是 seajs 推崇的规范,CMD 则是依赖就近,用的时候再 require
。
AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块。
15.4 ES6 Modules 规范
返回目录
- 导出:
export a
export { a }
export { a as jsliang }
export default function() {}
- 导入:
import './index'
import { a } from './index.js'
import { a as jsliang } from './index.js'
import * as index from './index.js'
特点:
export
命令和import
命令可以出现在模块的任何位置,只要处于模块顶层就可以。 如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。import
命令具有提升效果,会提升到整个模块的头部,首先执行。
和 CommonJS
区别:
CommonJS
模块是运行时加载,ES6 Modules
是编译时输出接口CommonJS
输出是值的拷贝;ES6 Modules
输出的是值的引用,被输出模块的内部的改变会影响引用的改变CommonJs
导入的模块路径可以是一个表达式,因为它使用的是require()
方法;而ES6 Modules
只能是字符串CommonJS this
指向当前模块,ES6 Modules
的this
指向undefined
ES6 Modules
中没有这些顶层变量:arguments
、require
、module
、exports
、__filename
、__dirname
十六 babel 编译原理
返回目录
babylon
将ES6/ES7
代码解析成AST
babel-traverse
对AST
进行遍历转译,得到新的AST
- 新
AST
通过babel-generator
转换成ES5
这一块的话 jsliang 并没有过分深究,单纯理解的话还是容易理解的:
- 黑白七巧板组成的形状,拆分出来得到零件(
ES6/ES7
解析成AST
) - 将这些零件换成彩色的(
AST
编译得到新AST
) - 将彩色零件拼装成新的形状(
AST
转换为ES5
)
十七 题集
返回目录
17.1 数组常见 API
返回目录
push
:数组尾部添加元素unshift
:数组头部添加元素pop
:数组尾部移除元素shift
:数组头部移除元素splice
:删除数组元素slice
:截取数组元素indexOf
:查找某元素第一次出现的位置lastIndexof
:查找某元素最后一次出现的位置findIndex
:查找元素第一次出现的位置forEach
:遍历元素map
:遍历元素filter
:过滤元素some
:包含某元素every
:所有元素和某元素一致includes
:查看是否包含某元素concat
:合并元素join
:合并元素,变成字符串toString
:变成字符串sort
:元素排序
17.2 常见 DOM API
返回目录
- 获取
- 创建
- 添加
- 删除
// 获取元素
const node = document.getElementById(id); // 或者 querySelector(".class|#id|name");
// 创建元素
const heading = document.createElement(name); // name: p、div、h1...
heading.innerHTML = '';
// 添加元素
document.body.appendChild(heading);
// 删除元素
document.body.removeChild(node);
17.3 数组去重
返回目录
数组去重是个经常提及的点:
const arr = [1, 1, 2, 3, 3];
// 期望得到:[1, 2, 3]
// 方法一:for 配合新数组截取
const newArr1 = [];
for (let i = 0; i < arr.length; i++) {
if (!newArr1.includes(arr[i])) {
newArr1.push(arr[i]);
}
}
console.log('newArr1:', newArr1);
// 方法二:使用 Set
const newArr2 = [...new Set(arr)];
console.log('newArr2:', newArr2);
// 方法三:使用 filter
const newArr3 = arr.filter((item, index) => arr.lastIndexOf(item) === index);
console.log('newArr3:', newArr3);
有一次面试碰到的有意思的提问是:不使用数组 API
进行去重。
注意:不能使用push
、indexOf
等API
17.4 数字化金额
返回目录
- 方法一:暴力遍历
const num = String(1234567890);
let result = '';
for (let i = num.length - 1; i >= 0; i--) {
if (i !== num.length - 1 && i % 3 === 0) {
result = num[i] + ',' + result;
} else {
result = num[i] + result;
}
}
console.log(result);
- 方法二:API 技巧
console.log(
String(1234567890).split('').reverse().reduce((prev, next, index) => (index % 3) === 0 ? next + ',' + prev : next + prev)
);
- 方法三:API技巧
console.log(
(1234567890).toLocaleString('en-US')
);
- 方法四:正则表达式
String(1234567890).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
17.5 遍历问题
返回目录
以下代码执行后,array 的结果是?
let array = [ , 1, , 2, , 3];
array = array.map((i) => ++i)
- A:
[ , 2, , 3, , 4]
- B:
[NaN, 2, NaN, 3, NaN, 4]
- C:
[1, 2, 1, 3, 1, 4]
- D:
[null, 2, null, 3, null, 4]
答案:A
解释:
forEach()
、filter()
、reduce()
、every()
和some()
都会跳过空位。map()
会跳过空位,但会保留这个值join()
和toString()
会将空位视为undefined
,而undefined
和null
会被处理成空字符串。
17.6 setTimeout
返回目录
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
以上代码执行结果?
- A:5 5 5 5 5
- B:0 0 0 0 0
- C:0 1 2 3 4
- D:1 2 3 4 5
答案:A
解析:
var i
在for
中使用,会造成变量污染,从而导致全局有一个遍历i
,这个i
运行到最后,就是5
setTimeout
是宏任务,在script
这个宏任务执行完毕后才执行,所以搜集到的i
是5
- 最终输出 5 个
5
17.7 requestAnimationFrame
返回目录
for (let i = 0; i < 5; i++) {
requestAnimationFrame(() => {
console.log(i);
});
}
以上代码执行结果:
- A:1 2 3 4 5
- B:0 1 2 3 4
- C:4 4 4 4 4
- D:5 5 5 5 5
答案:B
解析:
let i
使for
形成块级作用域。requestAnimationFrame
类似于setTimeout
,但是它可以当成一个微任务来看,是在微任务队列执行完毕后,执行 UI 渲染前,调用的一个方法。- 因此,这道题并不是指
requestAnimationFrame
会收集i
,而是let
形成了块级作用域的问题,如果改成var i
,照样输出 5 个5
。
17.8 暂时性死区
返回目录
1、下面代码输出什么?
let a = 1;
let test = function() {
console.log(a);
a++;
}
test();
2、下面代码输出什么?
let a = 1;
let test = function() {
console.log(a);
let a = 2;
a++;
}
test();
答案:
第一道题输出:1
第二道题输出:Uncaught ReferenceError: Cannot access 'a' before initialization
解析:
其原因是在同一个 block
中,let
在后面重新定义的,那么就不能在之前引用该变量。同时,也不能取嵌套外层的值。
17.9 输出打印结果
返回目录
function sayHi() {
console.log(name);
console.log(age);
var name = "Lydia";
let age = 21;
}
sayHi();
上面代码输出结果?
答案:undefined、报错
解析:
这道题转变一下就看明白了:
function sayHi() {
var name; // 变量提升 - 变量声明
console.log(name); // undefined
console.log(age); // let 存在暂时性死区,不会变量提升
name = "Lydia"; // 变量提升 - 变量赋值
let age = 21;
}
sayHi();
17.10 输出打印结果
返回目录
function myFunc() {
console.log(a);
console.log(func());
var a = 1;
function func() {
return 2;
}
}
myFunc();
请问输出啥?
答案:undefined
2
解析:不难,不解析了
17.11 Event Loop
返回目录
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1);
}
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1);
}
上面代码输出结果?
答案和解析:
第一道题目:var
在 for
中存在变量污染,同步代码 for
执行完毕之后,再执行宏任务 setTimeout
,发现当前 i
都成为 3
了,所以输出 3、3、3
第二道题目:let
在 for
中会形成块级作用域,每次迭代的时候 i
都是一个新值,并且每个值都存在于循环内的块级作用域,所以输出 0、1、2
17.12 输出打印结果
返回目录
let date = new Date();
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date - date, i); // 1
}, 1000);
}
console.log(new Date - date, i); // 2
请问输出啥?
答案:
0 5
1001 5
1004 5
1005 5
1006 5
1007 5
解析:题目先走宏任务 script
,所以定义了 date
之后,执行注释为 2 这行的 console
。
然后 5 个宏任务,都是定时器 setTimeout
,所以会在之后执行,输出:1000 5
,但是定时器也不一定准时的,所以有可能是 1001
、1002
或者其他的。
17.13 使 a == 1 && a == 2 成立
返回目录
尝试编码,使:if(a == 1 && a == 2 && a == 3) {}
这种情况成立。
- 方法一
在类型转换的时候,我们知道了对象如何转换成原始数据类型。如果部署了 [Symbol.toPrimitive]
,那么返回的就是 Symbol.toPrimitive
的返回值。
当然,我们也可以把此函数部署在 valueOf
或者是 toString
接口上,效果相同。
//利用闭包延长作用域的特性
let a = {
[Symbol.toPrimitive]: (function () {
let i = 1;
return function () {
return i++;
}
})()
}
- 方法二
利用 Object.defineProperty
在 window/global
上定义 a
属性,获取 a
属性时,会调用 get
let val = 1;
Object.defineProperty(window, 'a', {
get: function() {
return val++;
}
});
- 方法三
var a = [1, 2, 3];
a.join = a.shift;
数组的 toString
方法返回一个字符串,该字符串由数组中的每个元素的 toString()
返回值经调用 join()
方法连接(由逗号隔开)组成。
因此,我们可以重新 join
方法。返回第一个元素,并将其删除。
十八 More
返回目录
还有很多基础知识,或者题目,jsliang 没精力一一添加进来了,尽量将原文放文章中了。
- [x] [[译] 送你 43 道 JavaScript 面试题](https://juejin.im/post/684490...【阅读建议:1h】
这篇文章还是挺不错的,jsliang 做的第一遍还是错了一些题,哈哈。
那么本篇文章就到这里,祝小伙伴早日找到合适 Offer
。
<img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" />
<span xmlns:dct="http://purl.org/dc/terms/" property="dct:title">jsliang 的文档库</span> 由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于https://github.com/LiangJunro...上的作品创作。
本许可协议授权之外的使用权限可以从 https://creativecommons.org/l... 处获得。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。