一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
三 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

返回目录

可以使用 documentwindow 元素的 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

五 事件流

返回目录

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

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

如何让事件先冒泡后捕获:

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

5.1 addEventListener

返回目录

addEventListener 方法将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。

addEventListener 事件目标可以是文档上的元素 ElementDocumentWindow 或者任何其他支持事件的对象(例如 XMLHttpRequest)。

参考文档:EventTarget.addEventListener - MDN
  • 语法: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)和 IEDOM 事件产生顺序的描述。

网景 认为 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 区别

返回目录

image

这两者都是移入的时候触发,但是 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 拖拽

返回目录
  1. 通过 mousedownmousemovemouseup 方法实现
  2. 通过 HTML5 的 DragDrop 实现

十 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 个部分:

  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 次以上的代码才进行封装。

函数式编程就是对可以抽离的功能都进行抽取封装。

到这里仿佛掌握了真理,jsliang 也没详细了解定义撒,希望没误导。

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 中没有这些顶层变量:argumentsrequiremoduleexports__filename__dirname

十六 babel 编译原理

返回目录
  • babylonES6/ES7 代码解析成 AST
  • babel-traverseAST 进行遍历转译,得到新的 AST
  • AST 通过 babel-generator 转换成 ES5

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

  1. 黑白七巧板组成的形状,拆分出来得到零件(ES6/ES7 解析成 AST
  2. 将这些零件换成彩色的(AST 编译得到新 AST
  3. 将彩色零件拼装成新的形状(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 进行去重。

注意:不能使用 pushindexOfAPI

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

解释:

  1. forEach()filter()reduce()every()some() 都会跳过空位。
  2. map() 会跳过空位,但会保留这个值
  3. join()toString() 会将空位视为 undefined,而 undefinednull 会被处理成空字符串。

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

解析:

  1. var ifor 中使用,会造成变量污染,从而导致全局有一个遍历 i,这个 i 运行到最后,就是 5
  2. setTimeout 是宏任务,在 script 这个宏任务执行完毕后才执行,所以搜集到的 i5
  3. 最终输出 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

解析:

  1. let i 使 for 形成块级作用域。
  2. requestAnimationFrame 类似于 setTimeout,但是它可以当成一个微任务来看,是在微任务队列执行完毕后,执行 UI 渲染前,调用的一个方法。
  3. 因此,这道题并不是指 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);
}

上面代码输出结果?


答案和解析:

第一道题目:varfor 中存在变量污染,同步代码 for 执行完毕之后,再执行宏任务 setTimeout,发现当前 i 都成为 3 了,所以输出 3、3、3

第二道题目:letfor 中会形成块级作用域,每次迭代的时候 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,但是定时器也不一定准时的,所以有可能是 10011002 或者其他的。

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.definePropertywindow/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 没精力一一添加进来了,尽量将原文放文章中了。

这篇文章还是挺不错的,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... 处获得。

jsliang
393 声望31 粉丝

一个充满探索欲,喜欢折腾,乐于扩展自己知识面的终身学习斜杠程序员