JS面试题自测
# 什么是原始类型
- string
- number
- boolean
- null
- undefined
- symbol
# 什么是对象类型
除了原始类型外 如 array object ...
# 以上两者存储的区别
- 原始类型 -> 值
- 对象类型 -> 地址
# 深拷贝和浅拷贝指什么
深浅拷贝只针对 对象类型
- 浅拷贝只拷贝指向某个对象的指针 新旧对象共享一块内存
- 深拷贝创造一个一模一样的对象 不共享内存
# 赋值和浅拷贝的区别
- 赋值:赋值时赋的是地址 因此改变后两者联动改变
- 浅拷贝:创建一个新对象 扫描第一层 基本类型则拷贝值 对象类型则拷贝地址
# 浅拷贝实现方式 3 种
# 针对 Object:
Object.assign()
注意:obj 只有一层时是深拷贝
let obj = {
username: "kobe"
};
let obj2 = Object.assign({}, obj);
2
3
4
# ES6 新增:
- ...扩展运算符
# 针对 Array:
Array.prototype.concat()
let arr = [
1,
3,
{
username: "kobe"
}
];
let arr2 = arr.concat();
2
3
4
5
6
7
8
Array.prototype.slice()
let arr = [
1,
3,
{
username: " kobe"
}
];
let arr3 = arr.slice();
2
3
4
5
6
7
8
# 深拷贝的实现方式 3 种
JSON.parse(JSON.stringify())
- 会忽略属性值为 undefined 的属性
- 会忽略属性为 Symbol 的属性
- 不会序列化函数
- 不能解决循环引用的问题,直接报错
let arr = [
1,
3,
{
username: " kobe"
}
];
let arr4 = JSON.parse(JSON.stringify(arr));
2
3
4
5
6
7
8
- 函数库 lodash 中的
_.cloneDeep
var _ = require("lodash");
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false
2
3
4
5
6
7
8
9
- 手写递归实现深拷贝
- 判断 isObject
- 判断 isArray
- 浅拷贝第一层
- 遍历已浅拷贝的 key 所对应的的 value 是否有对象
- 如有则进入该对象迭代 无则赋原始对象
function deepClone(obj) {
function isObject(obj) {
return (
Object.prototype.toString.call(obj) === "[object Function]" ||
Object.prototype.toString.call(obj) === "[object Object]"
);
}
function isArray(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
if (!isObject(obj) && !isArray(obj)) {
return "error!";
}
let newObj = isObject(obj) ? { ...obj } : [...obj];
// 以上实现浅拷贝
// 循环第一层
Reflect.ownKeys(newObj).forEach(e => {
newObj[e] = isObject(newObj[e]) ? deepClone(newObj[e]) : newObj[e];
});
return newObj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# typeof 和 instanceof 的区别
# typeof
- 原始类型:
null
返回'object'
其余准确判断 - 对象类型:函数返回
function
其余返回'object'
# instanceof
- 通过原型链判断对象的类型 返回 boolean
[] instanceof Array; //true
{} instanceof Object;//true
new Date() instanceof Date;//true
new RegExp() instanceof RegExp//true
null instanceof Null//报错
undefined instanceof undefined//报错
2
3
4
5
6
# 还有什么数据类型判断的方法?
# constructor
constructor 作用和 instanceof 非常相似。但 constructor 检测 Object 与 instanceof 不一样,还可以处理基本数据类型的检测。 不过函数的 constructor 是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前的 constructor 给覆盖了,这样检测出来的结果就是不准确的。
# Object.prototype.toString.call() 最常用最准确
Object.prototype.toString.call(""); // [object String]
Object.prototype.toString.call(1); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(new Function()); // [object Function]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call(new RegExp()); // [object RegExp]
Object.prototype.toString.call(new Error()); // [object Error]
2
3
4
5
6
7
8
9
10
# JS 中有哪几种类型转换
3 种:转化成 数字 布尔值 字符串
# 分析以下代码
console.log([] == ![]); // true
1.左侧为空数组 -> object
2.右侧为![]
-> false
3.根据规则 布尔值需要变成数字
4.false -> 0 ; 空串 -> 0
# ==
与===
的区别
==
不严格相等:值相等即返回 true 涉及类型转换===
严格相等:值与类型都相等才返回 true
# 对象转原始类型的原理
调用内置[ToPrimitive]函数:
- 是否是原始类型 是则直接返回
- 调用
valueOf()
- 调用
toString()
- 如重写
Symbol.toPrimitive()
优先级最高 - 以上都没有返回原始类型则报错
# 对象转原始类型应用
Q:如何使if(a==1&&a==2&&a==3) {console.log('true')};
正确打印'true'
A:
let a = {
value: 0,
valueOf() {
return (this.value += 1);
}
};
2
3
4
5
6
# 对象有哪两类属性
ECMAScript 有两种属性:
数据属性:
- Configurable:能否使用 delete 删除属性
- Enumerable:能否使用 for-in
- Writable:能否修改属性值
- Value:包含属性的数据值
访问器属性:
- Configurable:同上
- Enumerable:同上
- Get
- Set
# new 构造调用的过程
- 创建新对象
- this 指向新对象
- 执行构造函数代码
- 返回新对象
# 什么是闭包
当函数可以记住并访问所在词法作用域时,就产生了闭包。
# 闭包有哪几种表现新式
- 返回函数
- 作为函数参数传递
- 回调函数
- IIFE
# 以下代码运行结果是什么?如何改进?
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
2
3
4
5
# 运行结果:每隔一秒输出一个 6
# 方法一:IIFE
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
2
3
4
5
6
7
# 方法二:let (推荐)
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
2
3
4
5
# 方法三:setTimeout 的第三个函数
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j);
},
i * 1000,
i
); //可以作为定时器执行时的变量进行使用
}
2
3
4
5
6
7
8
9
# 说说继承有哪几种方式?
- 原型链
- 借用构造函数
- 组合
- 寄生组合
- 类 (ES6)
# 简述原型链继承 (实现、优劣)
通俗的讲,该方法在定义完子的构造函数后强行绑定了一条“歪曲”的原型链。导致所有属性方法都是共享的,改变一个子类将会改变全部子类。
# 实现
改变子对象 prototype 指向 son.prototype = new Father()
// 原型继承
function father(name) {
this.name = name;
}
father.prototype.sayName = function() {
console.log(this.name);
};
function son(name) {
this.name = name;
}
son.prototype = new father("alex");
let son1 = new son("bob");
son1.sayName(); // bob
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 优劣
son.__proto__
本应等于father.prototype
,此处却丢失了
- 不能向父类构造函数传参
- constructor 不可靠
- 所有属性都是共享属性
# 简述借用构造函数实现继承(实现、优劣)
通俗的讲,该方法把父函数中定义私有属性的方法搬过来放到子函数中定义私有属性,仅此而已,所以并不能访问到共享属性。因为没有连接原型链。
把私有属性独立化出来了,所以不会产生所有属性皆共享的问题。
# 实现
在子类中使用 call 方法
function Son(name) {
Father.call(this, name);
}
var son1 = new Son("a");
son1 instanceof Father; // false
2
3
4
5
6
# 优劣
无法继承父类原型对象上的属性和方法 即无法继承 Father.prototype
# 简述组合继承(实现、优劣)(最常用)
通俗的讲,原型链继承可以访问共享方法但父函数的私有属性被迫共享,借用构造函数继承可以把私有属性独立出来不互相污染但无法访问共享属性。组合继承则兼顾了两者的优点。
继承方法 | 独立私有属性 | 访问共享方法 |
---|---|---|
原型链继承 | ❌ | ✅ |
借用构造函数 | ✅ | ❌ |
组合继承 | ✅ | ✅ |
# 实现
function Father(name) {
this.name = name;
}
Father.prototype.kill = () => {
console.log("killed");
};
function Son(name) {
Father.call(this, name); //第2次调用
}
Son.prototype = new Father(); //第1次调用
2
3
4
5
6
7
8
9
10
11
12
13
# 优劣
它保留了两种继承方式的优点,但它并不是百分百完美的:父类构造函数被调用多次。
- 第一次调用:Son.prototype 上会得到 name 属性
- 第二次调用:新对象创建了实例属性 name 并屏蔽之前得到的属性
# 简述寄生组合继承(实现、优劣)
通俗地讲,只是把 new father 用 object.create 实现。即只执行 new 中绑定原型链的一步从而舍去重复对父函数的调用。
🔥 关于 constructor:只有通过 new 生成的对象才有准确的构造函数
🔥Object.create: 将左边对象.proto绑定到右边对象
# 实现
function Animal(name) {
this.name = name;
this.colors = ["red", "blue"];
}
// 没有重写prototype不需要重新指定constructor
Animal.prototype.eat = function() {
console.log(this.name + " is eatting");
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype); //连接原型链
Dog.prototype.constructor = Dog; //指定构造函数
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 优劣
- 👍 公有的写在原型
- 👍 私有的写在构造函数
- 👍 可以向父类传递参数
- 👍 不会重复调用父类
- ❌ 需要手动绑定 constructor (如果重写 prototype)
- ❌ 需要调用额外的方法封装性一般
# 简述 ES6 class 继承
class Parent {
constructor(name, friends) {
// 该属性在构造函数上,不共享
this.name = name;
this.friends = friends;
}
log() {
// 该方法在原型上,共享
return this;
}
}
Parent.prototype.share = [1, 2, 3]; // 原型上的属性,共享
class Child extends Parent {
constructor(name, friends, gender) {
super(name, friends);
this.gender = gender;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# var let const 的区别
- var 会变量提升 let/const 不会
- var 声明的全局变量会被挂载到
window
对象 let/const 不会 - var 可以重复声明一个变量 let/const 不行
- var 函数作用域 let/const 块级作用域
# Array.from()方法
Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。
将一个类数组对象转换为一个真正的数组,必须具备以下条件:
- 该类数组对象必须具有 length 属性,用于指定数组的长度。如果没有 length 属性,那么转换后的数组是一个空数组。
- 该类数组对象的属性名必须为数值型或字符串型的数字
ps: 该类数组对象的属性名可以加引号,也可以不加引号
Array.from 还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组。如下:
let arr = [12, 45, 97, 9797, 564, 134, 45642];
let set = new Set(arr);
console.log(Array.from(set, item => item + 1)); // [ 13, 46, 98, 9798, 565, 135, 45643 ]
2
3
# set / map 数据结构
# Set
# 基本介绍
- ES6 提供了新的数据结构 Set。类似于数组,但成员值都是唯一的。
- 可以通过
add()
方法加入成员,或直接接收一个数组。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
const set = new Set([1, 2, 3, 4, 4]);
[...set];
// [1, 2, 3, 4]
2
3
4
5
6
7
8
9
10
11
12
- 以上方法可以用于数组去重或字符串去重
// 去除数组的重复成员
[...new Set(array)];
[...new Set("ababbc")].join("");
// "abc"
2
3
4
5
- 两个
NaN
相等 - 两个
{}
不相等
- 并集、交集、差集
以下代码的
Array.from(new Set())
目的都是先去重再转为数组对象
逻辑核心为new Set()
内部的代码
var arr1 = [1, 2, 1, 3, 4, 5];
var arr2 = [4, 5, 6, 7];
// 并集:输出1,2,3,4,5,6,7
var union = Array.from(new Set([...set1, ...set2]));
console.log(union);
// 交集:输出4,5
var intec = Array.from(new Set(arr2.filter(x => arr1.includes(x))));
console.log(intec);
// 差集
var diff1 = Array.from(new Set(arr1.filter(x => !arr2.includes(x))));
var diff2 = Array.from(new Set(arr2.filter(x => !arr1.includes(x))));
console.log(diff1); // 输出:1,2,3
console.log(diff2); // 输出:6,7
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 实例属性方法
操作方法:
Set.prototype.add(value):
添加某个值,返回 Set 结构本身。
Set.prototype.delete(value):
删除某个值,返回一个布尔值,表示删除是否成功。
Set.prototype.has(value):
返回一个布尔值,表示该值是否为 Set 的成员。
Set.prototype.clear():
清除所有成员,没有返回值。遍历方法:
Set.prototype.keys():
返回键名的遍历器
Set.prototype.values():
返回键值的遍历器
Set.prototype.entries():
返回键值对的遍历器
Set.prototype.forEach():
使用回调函数遍历每个成员
keys 方法、values 方法、entries 方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以 keys 方法和 values 方法的行为完全一致。
# Map
# 基本介绍
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
- ES6 提供了 Map 数据结构。各种类型的值都可以当做键。
- 可以使用
set()
方法添加成员或接收一个数组作为参数
const m = new Map();
const o = { p: "Hello World" };
m.set(o, "content");
m.get(o); // "content"
m.has(o); // true
m.delete(o); // true
m.has(o); // false
const map = new Map([
["name", "张三"],
["title", "Author"]
]);
map.size; // 2
map.has("name"); // true
map.get("name"); // "张三"
map.has("title"); // true
map.get("title"); // "Author"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 实例属性方法
size 属性
操作方法:
Map.prototype.set(key,value)
Map.prototype.get(key)
Map.prototype.has(key)
Map.prototype.delete(key)
Map.prototype.clear()
遍历方法:
Map.prototype.keys()
:返回键名的遍历器。
Map.prototype.values()
:返回键值的遍历器。
Map.prototype.entries()
:返回所有成员的遍历器。
Map.prototype.forEach()
:遍历 Map 的所有成员。
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
2
3
4
5
6
7
8
需要特别注意的是,Map 的遍历顺序就是插入顺序。
# Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
- Proxy 可以一次性为所有属性实现代理,无需遍历,性能更佳
- Proxy 能监听到以前使用 Object.definedProperty()监听不到的数据变动。
- 由于是 ES6 新增加的特性,所以浏览器兼容性方面比 Object.definedProperty()差
let onWatch = function(obj, setBind, getLogger) {
return new Proxy(obj, {
get(target, property, receiver) {
getLogger(target, property); // 加条console.log
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value, property); //加条console.log
return Reflect.set(target, property, value);
}
});
};
let obj = { a: 1 };
let p = onWatch(
obj,
(value, property) => {
console.log(`监听到${property}属性的改变,其值为${value}`);
},
(target, property) => {
console.log(`监听到获取属性${property},其值为${target[property]}`);
}
);
p.a = 2; // 监听到a属性的改变,其值为2
console.log(a); // 监听到获取属性a,其值为2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# map/filter/reduce 的区别
map 方法的作用是生成一个新数组(把原数组中的所有元素做一些变动,放进新数组中)
filter 方法的作用是从原数组中过滤出符合条件的元素,并生成一个新数组
reduce 方法的作用是通过回调函数的形式,把原数组中的元素最终转换成一个值,第一个参数是回调函数,第二个参数是初始值
方法 改变原数组 返回值 map ❌ 新数组 filter ❌ 新数组 reduce ❌ 值
# Promise
# 三种状态:
- 等待中 pending
- 完成 fulfilled
- 拒绝 rejected
# 工作原理:
- 生成实例
- 指定回调
// 创建一个promise实例 构造函数接收一个函数作为参数
var p1 = new Promise(Fn);
// 该函数 有两个函数作为参数
function Fn(resolve, reject) {
// 函数内部会定义什么时候让状态变成resolve或reject
if (异步操作成功) {
resolve(value);
} else {
reject(value);
}
}
// 以上便是实例生成
// 至此还未告知成功后做什么
// 实例生成后用then方法指定resolved状态和rejeceted状态的回调函数
p1.then(
val => {
// 成功后做什么
},
err => {
// 失败后做什么
}
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果后续任务是异步任务,必须 return 一个新的 promise 对象
# Generator
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
- function 关键字与函数名之间有一个星号
- 函数内部使用 yield 表达式
- 必须调用遍历器对象的 next 方法使指针移向下一个状态
const g = function*(x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
gen.throw(new Error("出错了")); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
next() | throw() | return |
---|---|---|
值 | throw 语句 | return 语句 |
# async/await
async 函数就是 Generator 函数的语法糖
*
->async
yield
->await
# 进程和线程的区别
- 进程:CPU 在运行指令及加载和保存上下文所需的时间
- 线程:进程中更小的单位,描述了执行一段指令所需的时间
# JS 单线程的好处
- 节省内存
- 节省上下文切换
- 没有锁的问题
# EventLoop
- 宏任务(
script、setTimeout、setInterval、setImmidiate、I/O、UI Rendering
)可以有多个队列 - 微任务(
procress.nextTick、Promise.then、Object.observe、mutataionObserver
)只能有一个队列
执行顺序:执行栈 -> 微任务 -> 宏任务
setTimeout(() => {
console.log(1);
Promise.resolve().then(() => {
console.log(2);
});
}, 0);
setTimeout(() => {
console.log(3);
}, 0);
Promise.resolve().then(() => {
console.log(4);
});
console.log(5);
// 输出结果:5 4 1 2 3
2
3
4
5
6
7
8
9
10
11
12
13
14
# CommonJS / AMD
# CommonJS
用 module.exports
定义当前模块对外输出的接口(不推荐直接用 exports),用 require
加载模块。
// 定义模块math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
add: add,
basicNum: basicNum
}
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);
// 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
commonJS 用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
# AMD
AMD 规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。
/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>;
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
jquery: "jquery.min", //实际路径为js/lib/jquery.min.js
underscore: "underscore.min"
}
});
// 执行基本操作
require(["jquery", "underscore"], function($, _) {
// some code here
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Module
ES6 的模块自动采用严格模式化
# export
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量
// true
export var m = 1;
export function (){}
// true
var m = 1;
export {m}
// true
var m = 1;
export {m as n}
// false
var m = 1;
export m
2
3
4
5
6
7
8
9
10
11
12
# import
import {m} from ''
import {m as n} from ''
import * from ''
2
3
4
5
# export default
从前面的例子可以看出,使用 import 命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
// export-default.js
export default function() {
console.log("foo");
}
// import-default.js
import customName from "./export-default";
customName(); // 'foo'
2
3
4
5
6
7
8