# 条件运算符 || 和 && 的返回结果是什么?

当操作数都是布尔值的时候,返回值也是布尔值;当操作数是非布尔值的时候,返回值就可能是非布尔值。

var o1 = true || true; // t || t returns true
var o2 = false || true; // f || t returns true
var o3 = true || false; // t || f returns true
var o4 = false || 3 == 4; // f || f returns false
var o5 = "Cat" || "Dog"; // t || t returns Cat
var o6 = false || "Cat"; // f || t returns Cat
var o7 = "Cat" || false; // t || f returns Cat
1
2
3
4
5
6
7
var a1 = true && true; // t && t returns true
var a2 = true && false; // t && f returns false
var a3 = false && true; // f && t returns false
var a4 = false && 3 == 4; // f && f returns false
var a5 = "Cat" && "Dog"; // t && t returns Dog
var a6 = false && "Cat"; // f && t returns false
var a7 = "Cat" && false; // t && f returns false
1
2
3
4
5
6
7

# ES6 filter 方法是在原数组基础上进行过滤的,还是生成一个新数组?过滤出来的新数组是深拷贝还是浅拷贝?

filter (opens new window) 在使用时会生成一个新数组,而不是在原数组上进行修改。

filter 方法创建的新数组包含的是原数组中满足条件的元素的引用。这意味着它是浅拷贝,新数组中的元素与原数组中的元素共享相同的对象引用。

# JS 中判断数据类型有哪些方法?

  • typeof 操作符

  • instanceof 操作符

  • Object.prototype.toString.call 方法

Object.prototype.toString.call(2); // '[object Number]'
Object.prototype.toString.call(true); // '[object Boolean]'
Object.prototype.toString.call("1"); // '[object String]'
Object.prototype.toString.call([]); // '[object Array]'
Object.prototype.toString.call({}); // '[object Object]'
Object.prototype.toString.call(null); // '[object Null]'
Object.prototype.toString.call(undefined); // '[object Undefined]'
1
2
3
4
5
6
7
  • Array.isArray 方法、isNaN 方法

# Object.keys、for in、for of 有什么区别

Object.keys 返回一个包含对象自身所有可枚举属性的数组,不会迭代对象的原型链上的属性,只返回对象自身的键,不包括继承的属性

for in 用于遍历对象的可枚举属性,包括自身和继承的属性。不建议用于遍历数组,因为它会遍历数组的原型链上的属性,且遍历顺序不一定按照属性被添加的顺序。

for of 用于遍历可迭代对象的值,例如数组、字符串、Map、Set 等。不会遍历对象的键,只返回值。不会遍历对象的原型链上的属性。

# script 的 async 和 defer 的区别

async 和 defer 都用于控制脚本的加载和执行时机,只对外部脚本有效,对内联的脚本无效。

  • async 脚本一旦下载完成,就会立即执行,不管 HTML 解析的进度。它不能保证脚本的执行顺序。

  • defer 脚本会在 HTML 解析完成后、DOMContentLoaded 事件触发前执行。它能够保证脚本按照原本的顺序执行。

当一个 script 标签中同时包含 defer 和 async 属性时,只会触发 async,不会触发 defer,除非浏览器不兼容 async。

# DOMContentLoaded 和 Load 的区别

DOMContentLoaded 和 load 事件都是与网页加载相关的事件,但它们触发的时机和应用场景有所不同。

  • DOMContentLoaded 事件在整个 HTML 文档被完全加载和解析之后触发,即 DOM 树构建完成时。这包括 HTML 内容、嵌套的图片、样式表、脚本文件等资源,但不包括外部资源如图片、嵌套的框架等的加载完成

  • load 事件在整个页面及其所有依赖资源(包括嵌套的资源如图片、样式表、脚本等)都完全加载后触发。这意味着页面上的所有元素、脚本、样式表、图片等都已经加载完成。

与 DOMContentLoaded 不同,load 事件会等待整个页面的所有资源都加载完毕,包括外部资源。因此,load 事件的触发通常比 DOMContentLoaded 慢。

# requestAnimationFrame 的用途和执行时机

requestAnimationFrame (opens new window) 是一个用于在浏览器中执行动画的 API。它的执行时机与浏览器的刷新频率相关,通常是每秒 60 次,但这也可能因设备或浏览器而异。

requestAnimationFrame 的执行时机与浏览器的屏幕刷新同步。浏览器每隔一定时间就会重绘屏幕,这个时间间隔通常是 16.67 毫秒(大约 60 帧/秒)。因此,requestAnimationFrame 的回调函数会在每次屏幕刷新之前执行。

requestAnimationFrame 的执行时机是由浏览器控制的,因此可以适应不同的硬件和性能条件。如果浏览器检测到无法以 60 帧/秒的速度绘制,它会自动调整刷新率,以避免卡顿或掉帧。

与传统的 setTimeout 或 setInterval 相比,requestAnimationFrame 更加高效。它能够利用浏览器的优化,避免掉帧和卡顿,并且在页面不可见时不会执行,减少不必要的计算开销。

requestAnimationFrame 不是微任务(microtask),它实际上是属于浏览器的渲染帧(rendering frame)的一部分,而微任务是 JavaScript 引擎的一部分。

# Promise 和 setTimeout 嵌套调用

function foo() {
  console.log(Math.random());
  return Promise.resolve().then(foo);
}
foo();
1
2
3
4
5
function foo() {
  console.log(Math.random());
  setTimeout(foo, 0);
}
foo();
1
2
3
4
5

第一段代码会导致页面很卡,这是因为虽然它使用的是微任务,但是 promise.resolve 的创建过程是同步的,而且微任务会在本次页面更新前执行,与同步执行无异,不会让出主线程,还是会阻塞到浏览器的主线程,从而导致卡顿。

而第二段代码中使用的宏任务就不会对浏览器主线程造成阻塞,因此不会卡顿。但是 setTimeout 嵌套调用会延迟定时器的执行时间,在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的最小时间间隔设置为 4 毫秒。

# forEach 和 for 循环有什么区别

1. 语法和用法

for 循环:是一种传统的迭代方式,使用明确的控制结构,需要指定循环的初始化、条件和步进。

for (let i = 0; i < array.length; i++) {
  console.log(array[i]);
}
1
2
3

forEach 方法:是数组原型上的方法,接受一个回调函数作为参数,不需要明确指定循环变量和循环条件。

array.forEach(function(item) {
  console.log(item);
});
1
2
3

2. 循环变量的作用域

  • for 循环:循环变量(如 i)在 for 循环体外仍然可见,可能会导致变量泄漏(例如,在闭包中)。

  • forEach 方法:回调函数中的变量具有块级作用域,不会影响到外部作用域。

3. 中断循环

  • for 循环:可以通过 break 语句中断循环。

  • forEach 方法:无法使用 break 来中断循环。如果需要在 forEach 中实现类似中断的效果,可以使用抛出异常或者返回 false 的方式。

try {
  array.forEach(function(item) {
    if (condition) {
      throw new Error("StopIteration");
    }
    console.log(item);
  });
} catch (e) {
  if (e.message !== "StopIteration") throw e;
}
1
2
3
4
5
6
7
8
9
10

4. 返回值

  • for 循环:不会返回任何值。

  • forEach 方法:不会返回任何值(返回 undefined)。如果需要创建一个新的数组或进行一些数据处理,可以使用 map 方法。

const newArray = array.map(function(item) {
  return item * 2;
});
1
2
3

# new Object 和 Object.create 的区别

new Object() 是使用构造函数 Object 来创建一个空对象的实例,返回一个新的空对象。

var obj = new Object(); // 或者可以简写为 var obj = {};
1

Object.create() 是使用指定的原型对象来创建一个新对象,可以通过传递第一个参数作为原型,创建具有指定原型的对象。

var obj = Object.create(null); // 创建一个没有原型的对象
var anotherObj = Object.create({ prop: 1 }); // 创建一个原型为 { prop: 1 } 的对象
1
2

关键区别:

  • new Object() 创建的对象是基于 Object 构造函数的实例,它继承了 Object 构造函数的原型。

  • Object.create() 创建的对象的原型可以被明确指定,可以选择继承一个特定的对象。

总体而言,new Object() 适用于创建简单的空对象,而 Object.create() 更适用于创建具有指定原型链的对象。

# 箭头函数和普通函数的区别

  1. 箭头函数语法比普通函数更加简洁。

  2. 箭头函数没有自己的 this,只能通过作用域链向上查找离自己最近的那个函数的 this。

  3. 箭头函数不能作为构造函数,因此不能通过 new 来调用。

  4. 箭头函数没有 arguments,可以通过 rest 的方式(...)获取参数。

  5. 箭头函数不能直接使用 call、apply 和 bind 来改变 this。

  6. 箭头函数不能使用 yield 关键字,不能作为 Generator 函数。

  7. 箭头函数没有原型属性(prototype)。

# const 和 Object.freeze() 的区别

  1. const 不能改变对象或数组的引用,但可以改变它们的属性值。

  2. Object.freeze() 不能改变对象或数组的属性值,但可以改变它们的引用。而且 Object.freeze() 是浅层冻结,嵌套的对象是不会被冻结的,此时需要通过递归使用它来冻结整个对象。

  3. const 和 Object.freeze() 结合一起使用能实现一个属性值和引用都不可变的对象。

# JS 精度丢失的原因和解决方法

0.1 + 0.2 = 0.30000000000000004;
1

# 原因

在 JS 中,所有数字包括整数和小数都只有一种类型 —— Number,它使用的是双精度浮点型,也就是其他语言中的 double 类型,而双精度浮点数使用 64 bit 进行存储。这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。

首先 0.1 和 0.2 会先被转成二进制,而 0.1 和 0.2 转成二进制之后都是一个无限循环的数,而 IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位,这时候就必须来进行四舍五入了,而这个取舍的规则也是在 IEEE 754 中定义的。最终加完之后这个二进制数转成十进制就是 0.30000000000000004。

计算机中用二进制来存储小数,而大部分小数转成二进制之后都是无限循环的值,因此存在取舍问题,也就是精度丢失。

# 解决方法

  1. 使用一些数学类库,比如:math.js (opens new window)decimal.js (opens new window) 等等,如果觉得这些包太大的话,还可以使用 number-precision (opens new window),这个工具库的 API 简洁很多,已经可以解决浮点数的计算问题了。

math.js、decimal.js、number-precision 解决精度丢失问题的原理

  1. math.js 是一个广泛用于执行数学运算的库,它提供了大量的数学函数和工具,并没有特别针对精度问题进行设计,使用 JavaScript 原生的浮点数进行运算。要解决精度问题,通常需要使用 math.js 提供的 bignumber 模块,该模块基于字符串表示大数进行运算,从而绕过 JavaScript 浮点数精度的限制。
const math = require("mathjs");

const a = math.bignumber(0.1);
const b = math.bignumber(0.2);
const result = math.add(a, b);

console.log(result.toString()); // 输出 '0.3'
1
2
3
4
5
6
7
  1. decimal.js 是专门设计用于高精度计算的库,它的原理是采用了固定小数点的算法,将数字以字符串形式存储,通过字符串的运算来维护精度。
const Decimal = require("decimal.js");

const a = new Decimal("0.1");
const b = new Decimal("0.2");
const result = a.plus(b);

console.log(result.toString()); // 输出 '0.3'
1
2
3
4
5
6
7
  1. number-precision 的核心原理是通过将数字乘以 10 的 n 次方,将浮点数转化为整数进行运算,然后再将结果除以 10 的 n 次方,转回浮点数。
const NP = require("number-precision");

const result = NP.plus(0.1, 0.2);

console.log(result); // 输出 0.3
1
2
3
4
5

前两个库都是通过使用字符串表示数字进行运算,绕过 JavaScript 浮点数精度问题。而 number-precision 的实现方式较为简单,它通过放大整数运算来处理浮点数精度问题,适用于一些简单的场景。

  1. 使用原生方法 toFixed() (opens new window),但这个方法并不可靠,会有兼容性问题。解决 toFixed() 兼容性问题的方法是:重写 toFixed 方法,通过判断最后一位是否大于等于 5 来决定需不需要进位,如果需要进位先把小数乘以倍数变为整数,加 1 之后,再除以倍数变为小数,这样就不用一位一位的进行判断。参考:js 中小数四舍五入和浮点数的研究 (opens new window)

# 实现数组扁平化

  1. 递归实现
function fn(arr) {
  let arr1 = [];
  arr.forEach((val) => {
    if (val instanceof Array) {
      arr1 = arr1.concat(fn(val));
    } else {
      arr1.push(val);
    }
  });
  return arr1;
}
1
2
3
4
5
6
7
8
9
10
11
  1. 使用 reduce 实现
function fn(arr) {
  return arr.reduce((prev, cur) => {
    return prev.concat(Array.isArray(cur) ? fn(cur) : cur);
  }, []);
}
1
2
3
4
5
  1. 使用 flat,参数为层数,默认只展开一层
arr.flat(Infinity);
1
  1. 扩展运算符
function fn(arr) {
  let arr1 = [];
  let flag = true;
  arr.forEach((val) => {
    if (Array.isArray(val)) {
      arr1.push(...val);
      flag = false;
    } else {
      arr1.push(val);
    }
  });
  if (flag) {
    return arr1;
  }
  return fn(arr1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. toString
let arr1 = arr
  .toString()
  .split(",")
  .map((val) => {
    return parseInt(val);
  });
console.log(arr1);
1
2
3
4
5
6
7
  1. apply
function flattern(arr) {
  // some() 方法用于检测数组中的元素是否满足指定条件
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat.apply([], arr);
  }
  return arr;
}
1
2
3
4
5
6
7

# JS 闭包

JS 闭包 (opens new window)

# JS 实现继承的方法

// 定义一个动物类
function Animal(name) {
  // 属性
  this.name = name || "Animal";
  // 实例方法
  this.sleep = function() {
    console.log(this.name + "正在睡觉!");
  };
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + "正在吃:" + food);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 原型链继承

利用 对象.__proto__ === 构造函数.prototype

function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.name = "cat";
1
2
3
  1. 构造继承
function Cat(name) {
  Animal.call(this);
  this.name = name || "Tom";
}
1
2
3
4
  1. 实例继承
function Cat(name) {
  var instance = new Animal();
  instance.name = name || "Tom";
  return instance;
}
1
2
3
4
5
  1. 拷贝继承
function Cat(name) {
  var animal = new Animal();
  for (var p in animal) {
    Cat.prototype[p] = animal[p];
  }
}
1
2
3
4
5
6

关于其他的继承实现方式以及各自的优缺点详见此处 (opens new window)

除此之外,还可以使用 es6 新增的 class 关键字实现继承。继承的方法就是通过 extends 关键字。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return this.x + " " + this.y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    // 调用父类的 constructor(x, y),在这里表示父类的构造函数,用来新建父类的this对象。
    super(x, y);
    this.color = color;
  }

  toString() {
    return this.color + " " + super.toString(); // 调用父类的 toString()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))

ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this

如果子类没有定义 constructor 方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor 方法。

class ColorPoint extends Point {}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}
1
2
3
4
5
6
7
8

另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

实例化

let cp = new ColorPoint(25, 8, "green");

cp instanceof ColorPoint; // true
cp instanceof Point; // true
1
2
3
4

实例对象 cp 同时是 ColorPoint 和 Point 两个类的实例,这与 ES5 的行为完全一致。

父类的静态方法,也会被子类继承。

class A {
  static hello() {
    console.log("hello world");
  }
}

class B extends A {}

B.hello(); // hello world
1
2
3
4
5
6
7
8
9

参考自ES6 Class 的继承 (opens new window)

# 使用 JS 实现路由功能

简单实现如下,当然实际应用中肯定不是这么简单。

  1. url 中的 hash 就是指 # 号以及 # 号后面的内容。

  2. 使用 a 标签的 href 属性,跳转地址带上 # 内容,就可以跳到对应的路由地址。

<a href="#/aaa">AAA</a>
1
  1. 当跳到对应的路由地址时,显示对应的路由页面。实际上就是判断 location.hash 是哪个,然后根据这个值来给页面添加内容。监听 hash 值变化的函数就是 onhashchange
window.onhashchange = function() {
  let hash = location.hash;
  hash = hash.replace("#", "");
  switch (hash) {
    case "/aaa":
      div.innerHTML = "AAAAAA";
      break;
    default:
      div.innerHTML = "";
  }
};
1
2
3
4
5
6
7
8
9
10
11

# JS 的事件模型是什么样的

DOM 事件机制 (opens new window)

# JS 利用高阶函数实现 memorize 函数缓存(备忘模式)

const memorize = function(fn) {
  const cache = {};
  return function(...args) {
    const _args = JSON.stringify(args);
    return cache[_args] || (cache[_args] = fn.apply(fn, args));
  };
};
const add = function(a) {
  return a + 1;
};
const adder = memorize(add);
console.log(adder(1)); // 2 cache: { '[1]': 2 }
console.log(adder(2)); // 3 cache: { '[1]': 2, '[2]': 3 }
console.log(adder(1)); // 2 cache: { '[1]': 2, '[2]': 3 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 使用 promise 实现 sleep 函数

async function test() {
  for (var i = 0; i < 10; i++) {
    await sleep(1000);

    console.log(i);
  }
}

function sleep(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        resolve(1);
      } catch (e) {
        reject(0);
      }
    }, delay);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 44 道比较难的 JS 面试题

44 道比较难的 JS 面试题 (opens new window)

# 面试常见的手写功能

面试常见的手写功能 (opens new window)

# 前端面试常考的各种重点手写题大全

前端面试常考的各种重点手写题大全 (opens new window)

上次更新时间: 2024年01月10日 17:20:13