javascript 基础总结
一 目录
加油!!!
目录 |
---|
一 目录 |
二 前言 |
三 DOM 常用 API |
四 null 和 undefined 的区别 |
五 事件流 |
5.1 addEventListener |
5.2 原理 |
5.3 案例 |
5.4 练习题 |
5.5 阻止冒泡 |
5.6 onmouseover 和 onmouseenter 区别 |
5.7 科普 |
六 typeof 和 instanceof 的区别 |
七 一句话描述 this |
八 JS 位置 |
九 JS 拖拽 |
十 setTimeout 实现 setInterval |
十一 实现 Sleep |
十二 执行上下文 |
12.1 执行上下文类型 |
12.2 执行栈 |
十三 函数式编程 |
13.1 函数式编程特点 |
13.2 纯函数 |
十四 渐进式网络应用(PWA) |
14.1 优点 |
14.2 缺点 |
十五 规范化 |
15.1 CommonJS 规范 |
15.2 AMD 规范 |
15.3 CMD 规范 |
15.4 ES6 Modules 规范 |
十六 babel 编译原理 |
十七 题集 |
17.1 数组常见 API |
17.2 常见 DOM API |
17.3 数组去重 |
17.4 数字化金额 |
17.5 遍历问题 |
17.6 setTimeout |
17.7 requestAnimationFrame |
17.8 暂时性死区 |
17.9 输出打印结果 |
17.10 输出打印结果 |
17.11 Event Loop |
17.12 输出打印结果 |
17.13 使 a == 1 && a == 2 成立 |
十八 More |
二 前言
https://product.mdnice.com/themes/
在 JavaScript 复习过程中,可能会碰到:
1.null
和 undefined
的区别?
2.addEventListener
函数?
这样杂七杂八的问题,亦或者 a == 1 && a == 2
这样有趣的问题。
将它们归类到 JavaScript 基础,并在本篇文章中一一讲述。
同时,会有十几道简单题目练手。
三 DOM 常用 API
3.1 标题
可以使用 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
。
五 事件流
什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2级事件流包括下面几个阶段。
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
如何让事件先冒泡后获取:
再DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。
5.1 addEventListener
addEventListener
方法将指定的监听器注册到 EventTarget
上,当该对象触发指定的事件时,指定的回调函数就会被执行。
addEventListener
事件目标可以是文档上的元素 Element、Document 和 Window 或者任何其他支持事件的对象(例如 XMLHttpRequest)。
- 语法:
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()
进行变量判断。
七 一句话描述 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
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 次以上的代码才进行封装。
函数式编程就是对可以抽离的功能都进行抽取封装。
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
这一块的话我并没有过分深究,单纯理解的话还是容易理解的:
- 黑白七巧板组成的形状,拆分出来得到零件(ES6/ES7 解析成 AST)
- 将这些零件换成彩色的(AST 编译得到新 AST)
- 将彩色零件拼装成新的形状(AST 转换为 ES5)