# 条件运算符 || 和 && 的返回结果是什么?
当操作数都是布尔值的时候,返回值也是布尔值;当操作数是非布尔值的时候,返回值就可能是非布尔值。
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
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
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]'
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();
2
3
4
5
function foo() {
console.log(Math.random());
setTimeout(foo, 0);
}
foo();
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]);
}
2
3
forEach 方法:是数组原型上的方法,接受一个回调函数作为参数,不需要明确指定循环变量和循环条件。
array.forEach(function(item) {
console.log(item);
});
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;
}
2
3
4
5
6
7
8
9
10
4. 返回值
for 循环:不会返回任何值。
forEach 方法:不会返回任何值(返回 undefined)。如果需要创建一个新的数组或进行一些数据处理,可以使用 map 方法。
const newArray = array.map(function(item) {
return item * 2;
});
2
3
# new Object 和 Object.create 的区别
new Object() 是使用构造函数 Object 来创建一个空对象的实例,返回一个新的空对象。
var obj = new Object(); // 或者可以简写为 var obj = {};
Object.create() 是使用指定的原型对象来创建一个新对象,可以通过传递第一个参数作为原型,创建具有指定原型的对象。
var obj = Object.create(null); // 创建一个没有原型的对象
var anotherObj = Object.create({ prop: 1 }); // 创建一个原型为 { prop: 1 } 的对象
2
关键区别:
new Object() 创建的对象是基于 Object 构造函数的实例,它继承了 Object 构造函数的原型。
Object.create() 创建的对象的原型可以被明确指定,可以选择继承一个特定的对象。
总体而言,new Object() 适用于创建简单的空对象,而 Object.create() 更适用于创建具有指定原型链的对象。
# 箭头函数和普通函数的区别
箭头函数语法比普通函数更加简洁。
箭头函数没有自己的 this,只能通过作用域链向上查找离自己最近的那个函数的 this。
箭头函数不能作为构造函数,因此不能通过 new 来调用。
箭头函数没有 arguments,可以通过 rest 的方式(...)获取参数。
箭头函数不能直接使用 call、apply 和 bind 来改变 this。
箭头函数不能使用 yield 关键字,不能作为 Generator 函数。
箭头函数没有原型属性(prototype)。
# const 和 Object.freeze() 的区别
const 不能改变对象或数组的引用,但可以改变它们的属性值。
Object.freeze() 不能改变对象或数组的属性值,但可以改变它们的引用。而且 Object.freeze() 是浅层冻结,嵌套的对象是不会被冻结的,此时需要通过递归使用它来冻结整个对象。
const 和 Object.freeze() 结合一起使用能实现一个属性值和引用都不可变的对象。
# JS 精度丢失的原因和解决方法
0.1 + 0.2 = 0.30000000000000004;
# 原因
在 JS 中,所有数字包括整数和小数都只有一种类型 —— Number,它使用的是双精度浮点型,也就是其他语言中的 double 类型,而双精度浮点数使用 64 bit 进行存储。这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。
首先 0.1 和 0.2 会先被转成二进制,而 0.1 和 0.2 转成二进制之后都是一个无限循环的数,而 IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位,这时候就必须来进行四舍五入了,而这个取舍的规则也是在 IEEE 754 中定义的。最终加完之后这个二进制数转成十进制就是 0.30000000000000004。
计算机中用二进制来存储小数,而大部分小数转成二进制之后都是无限循环的值,因此存在取舍问题,也就是精度丢失。
# 解决方法
- 使用一些数学类库,比如:math.js (opens new window)、decimal.js (opens new window) 等等,如果觉得这些包太大的话,还可以使用 number-precision (opens new window),这个工具库的 API 简洁很多,已经可以解决浮点数的计算问题了。
math.js、decimal.js、number-precision 解决精度丢失问题的原理
- 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'
2
3
4
5
6
7
- 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'
2
3
4
5
6
7
- number-precision 的核心原理是通过将数字乘以 10 的 n 次方,将浮点数转化为整数进行运算,然后再将结果除以 10 的 n 次方,转回浮点数。
const NP = require("number-precision");
const result = NP.plus(0.1, 0.2);
console.log(result); // 输出 0.3
2
3
4
5
前两个库都是通过使用字符串表示数字进行运算,绕过 JavaScript 浮点数精度问题。而 number-precision 的实现方式较为简单,它通过放大整数运算来处理浮点数精度问题,适用于一些简单的场景。
- 使用原生方法 toFixed() (opens new window),但这个方法并不可靠,会有兼容性问题。解决 toFixed() 兼容性问题的方法是:重写 toFixed 方法,通过判断最后一位是否大于等于 5 来决定需不需要进位,如果需要进位先把小数乘以倍数变为整数,加 1 之后,再除以倍数变为小数,这样就不用一位一位的进行判断。参考:js 中小数四舍五入和浮点数的研究 (opens new window)
# 实现数组扁平化
- 递归实现
function fn(arr) {
let arr1 = [];
arr.forEach((val) => {
if (val instanceof Array) {
arr1 = arr1.concat(fn(val));
} else {
arr1.push(val);
}
});
return arr1;
}
2
3
4
5
6
7
8
9
10
11
- 使用 reduce 实现
function fn(arr) {
return arr.reduce((prev, cur) => {
return prev.concat(Array.isArray(cur) ? fn(cur) : cur);
}, []);
}
2
3
4
5
- 使用 flat,参数为层数,默认只展开一层
arr.flat(Infinity);
- 扩展运算符
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- toString
let arr1 = arr
.toString()
.split(",")
.map((val) => {
return parseInt(val);
});
console.log(arr1);
2
3
4
5
6
7
- apply
function flattern(arr) {
// some() 方法用于检测数组中的元素是否满足指定条件
while (arr.some((item) => Array.isArray(item))) {
arr = [].concat.apply([], arr);
}
return arr;
}
2
3
4
5
6
7
# JS 闭包
# 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);
};
2
3
4
5
6
7
8
9
10
11
12
13
- 原型链继承
利用 对象.__proto__ === 构造函数.prototype
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.name = "cat";
2
3
- 构造继承
function Cat(name) {
Animal.call(this);
this.name = name || "Tom";
}
2
3
4
- 实例继承
function Cat(name) {
var instance = new Animal();
instance.name = name || "Tom";
return instance;
}
2
3
4
5
- 拷贝继承
function Cat(name) {
var animal = new Animal();
for (var p in animal) {
Cat.prototype[p] = animal[p];
}
}
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()
}
}
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);
}
}
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; // 正确
}
}
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
2
3
4
实例对象 cp 同时是 ColorPoint 和 Point 两个类的实例,这与 ES5 的行为完全一致。
父类的静态方法,也会被子类继承。
class A {
static hello() {
console.log("hello world");
}
}
class B extends A {}
B.hello(); // hello world
2
3
4
5
6
7
8
9
参考自ES6 Class 的继承 (opens new window)
# 使用 JS 实现路由功能
简单实现如下,当然实际应用中肯定不是这么简单。
url 中的 hash 就是指 # 号以及 # 号后面的内容。
使用 a 标签的 href 属性,跳转地址带上 # 内容,就可以跳到对应的路由地址。
<a href="#/aaa">AAA</a>
- 当跳到对应的路由地址时,显示对应的路由页面。实际上就是判断
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 = "";
}
};
2
3
4
5
6
7
8
9
10
11
# JS 的事件模型是什么样的
# 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 }
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);
});
}
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)