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 |
二 前言
在 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 科普
并不是所有的事件都有冒泡,例如:
onbluronfocusonmouseenteronmouseleave
六 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的厚度,在未指定的情况下一般为0scrollTop:滚动后被隐藏的高度,获取对象相对于由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 aexport { 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)
十七 题集
作者:jsliang 链接:juejin.cn/post/689035… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。