javascript 基础总结

370 阅读16分钟

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.nullundefined 的区别?
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:
  1. 作为函数的参数,表示该函数的参数不是对象。
  2. 作为对象原型链的终点。Object.prototype.__proto__ === null
  • undefined:
  1. 变量被声明但是没有赋值,等于 undefined
  2. 调用函数时,对应的参数没有提供,也是 undefined
  3. 对象没有赋值,这个属性的值为 undefined
  4. 函数没有返回值,默认返回undefined

五 事件流

什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2级事件流包括下面几个阶段。

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

如何让事件先冒泡后获取: 再DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。

5.1 addEventListener

addEventListener 方法将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。 addEventListener 事件目标可以是文档上的元素 Element、Document 和 Window 或者任何其他支持事件的对象(例如 XMLHttpRequest)。

  • 语法:target.addEventListener(type, listener, options/useCapture)
  1. type:表示监听事件类型的字符串

  2. listener:所监听的事件触发,会接受一个事件通知对象。

  3. options:一个指定有关 listener 属性的可选参数对象。可选值有 capture(事件捕获阶段传播到这里触发)、once(在 listener 添加之后最多值调用一次)、passive(设置为 true 时表示 listener 永远不会调用 preventDefault()

  4. 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 显示错误是个历史 Bugtypeof 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 个部分:

  1. 拆分数组中字符串,将字符串变成人名。john-reese -> John Reese
  2. 将数组转换成对象。['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 函数式编程特点

  1. 函数是一等公民。可以利用这点让它支持抽取到外部。
  2. 声明做某件时间。函数式编程大多数声明某个函数需要做什么,而不是它怎么做的。
  3. 便于垃圾回收。函数内部的变量方便垃圾回收,不会产生太多的变量,用户不需要大量的定义。
  4. 数据不可变。函数式编程要求所有的数据都是不可变的,如果需要修改某个对象,应该新建后再修改,而不是污染原本的数据。
  5. 无状态。不管什么时候运行,同一个函数对相同的输入返回相同的输出,而不依赖外部状态的变化。
  6. 无副作用。功能 A 应该仅仅为了完成它的实现,而不会随着外部的改变而改变,这样当它执行完毕之后,就可以将其内部数据进行回收。并且它不会修改传入的参数。

注重引用值(Object、Array)的传递,尽可能不要污染传入的数据。

13.2 纯函数

纯函数的概念有 2 点:

  1. 不依赖外部状态(无状态):函数的运行结果不依赖全局变量,this 指针,IO 操作等。
  2. 没有副作用(数据不变):不修改全局变量,不修改入参。 优点:

便于测试和优化 可缓存性 自文档化 更少 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 目录查找。

它的特点:

  1. 所有代码在模块作用域内运行,不会污染其他文件
  2. require 得到的值是值的拷贝,即你引用其他 JS 文件的变量,修改操作了也不会影响其他文件

它也有自己的缺陷:

  1. 应用层面。在 index.html 中做 var index = require('./index.js') 操作报错,因为它最终是后台执行的,只能是 index.js 引用 index2.js 这种方式。
  2. 同步加载问题。CommonJS 规范中模块是同步加载的,即在 index.js 中加载 index2.js,如果 index2.js 卡住了,那就要等很久。

15.2 AMD 规范

为什么有 AMD 规范? 答:CommonJS 规范不中用:

  1. 适用客户端
  2. 等待加载(同步加载问题)。

所以它做了啥? 可以采用异步方式加载模块。AMDAsynchronous Module Definition的缩写,也就是 “异步模块定义”,记住这个 async 就知道它是异步的了。

15.3 CMD 规范

CMD (Common Module Definition), 是 seajs 推崇的规范,CMD 则是依赖就近,用的时候再 require。 AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块。

15.4 ES6 Modules 规范

  • 导出:
  1. export a
  2. export { a }
  3. export { a as jsliang }
  4. export default function() {}
  • 导入:
  1. import './index'
  2. import { a } from './index.js'
  3. import { a as jsliang } from './index.js'
  4. import * as index from './index.js'
  • 特点:
  1. export 命令和 import 命令可以出现在模块的任何位置,只要处于模块顶层就可以。 如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
  2. import 命令具有提升效果,会提升到整个模块的头部,首先执行。
  • CommonJS 区别:

  • CommonJS 模块是运行时加载,ES6 Modules 是编译时输出接口

  • CommonJS 输出是值的拷贝;ES6 Modules 输出的是值的引用,被输出模块的内部的改变会影响引用的改变

  • CommonJs 导入的模块路径可以是一个表达式,因为它使用的是require() 方法;而 ES6 Modules 只能是字符串 - CommonJS this 指向当前模块,ES6 Modulesthis 指向 undefined

  • ES6 Modules 中没有这些顶层变量:arguments、require、module、exports、__filename、__dirname

十六 babel 编译原理

  • babylon 将 ES6/ES7 代码解析成 AST
  • babel-traverse 对 AST 进行遍历转译,得到新的 AST
  • 新 AST 通过 babel-generator 转换成 ES5

这一块的话我并没有过分深究,单纯理解的话还是容易理解的:

  1. 黑白七巧板组成的形状,拆分出来得到零件(ES6/ES7 解析成 AST)
  2. 将这些零件换成彩色的(AST 编译得到新 AST)
  3. 将彩色零件拼装成新的形状(AST 转换为 ES5)

十七 题集

作者:jsliang 链接:juejin.cn/post/689035… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。