javascript 面试题(持续更新)

javascript 面试题(持续更新)
mu数据类型相关
js有多少种数据类型
- 7种原始数据类型:null、undefined、boolean、number、string、symbol(es6新增)、bigint(es10新增)
- 1种引用类型:object
Symbol的特点和作用
- 每个 symbol 实例值都是唯一的
- 不能通过 new 构造
应用场景:var a = Symbol(); var b = Symbol(); a == b // false var c = new Symbol() // Uncaught TypeError: Symbol is not a constructor
- 作为对象属性名
Symbol 类型的属性时不可被枚举的,Object.keys()、for…in、Object.getOwnPropertyNames() 都不能枚举出 Symbol,利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol 来定义var obj = { a: 1, [Symbol()]: 'symbol' } Object.keys(obj); // ['a']
- 使用 Symbol 来替代常量
- 定义类的私有属性/方法
BigInt的特点和作用
BigInt 可以表示任意大的整数
可以在整数后面加 n 定义一个 BigInt (如10n),或调用 BigInt()(不需要new)
特定:
- bigint 不能用于 Math 对象中的方法
- 不能和 Number 实例混合运算
- 带小数的运算会被取整
- BigInt 和 Number 不严格相等
应用场景:高精度时间戳,大整数
null和undefined有什么区别
typeof null // 'object'
typeof undefined // 'undefined'
null == undefined // true
null === undefined // false
Number(null) // 0
Number(undefined) // NaN
关于 typeof null 为 object 的 bug,早起版本用低位存储变量的类型,000表示对象,而 null 全为零,所以将 null 判断为了object
null 是空对象指针,转换为数值为0;undefined 是表示”无”的原始值,转为数值时为 NaN
怎么判断数据类型
用 typeof 判断基本数据类型和函数,typeof function(){} // ‘function’
数组和对象用 typeof 操作结果都是 object,区分数组和对象的方式有:
方式一:instanceof
typeof {} // 'object'
typeof [] // 'object'
({}) instanceof Object // true
[] instanceof Array // true
注意:{} instanceof Object会报错【Uncaught SyntaxError: Unexpected token ‘instanceof’】,因为{}也可看做代码块,应给{}加个括号,写成 ({}) instanceof Object
方式二:constructor属性
var arr = [1,2]
arr.constructor === Array // true
var obj = {}
obj.constructor === Object // true
方式三:Array.isArray()
方式四:[[JS对象#Object.prototype.toString.call()]] 可以区分所有类型
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call([])); // [object Array]
怎么进行数据类型转换
- 转换为数字:Number()、parseInt()、parseFloat()
- 转换为字符串:toString()、String()
- 转换为布尔:Boolean()
Number(null) // 0 Number(undefined) // NaN Number('') // 0 Number(3-1) // 2 Number('3'-'1') // 2 这样竟然也可以 Number('1a') // NaN parseInt(null) // NaN parseInt(undefined) // NaN parseInt('') // NaN parseInt('3.9'-'2') // 1 parseInt('a') // NaN parseInt('123abc') // 123 Boolean('') // false Boolean('123') // true Boolean('true') // true Boolean('false') // true Boolean('0') // true Boolean(0) // false Boolean(null) // false Boolean(undefined) // false
NaN是什么值
Not a Number,是一个表示非数字的值,不可写不可枚举不可配置
typeof NaN // 'number'
NaN instanceof Number // false
NaN === NaN // false
判断 NaN,可以使用 isNaN() 或 Number.isNaN(),或者,因为 NaN 是唯一与自身不相等的值,所以可以执行类似 x !== x 这样的自我比较
isNaN(NaN); // true
isNaN(Number.NaN); // true
Number.isNaN(NaN); // true
// isNaN()和Number.isNaN()之间有区别
isNaN("hello world"); // true
Number.isNaN("hello world"); // false,仅当值为NaN才为true
原型、原型链、继承
原型和原型链
JavaScript 中的对象有一个特殊的 [[Prototype]]
内置属性,就是对其他对象的引用,如果在对象上没有找到需要的属性或方法,就会继续在 [[Prototype]]
关联的对象上查找,层层向上找直到找到或者到对象原型为null时结束,这一系列 [[Prototype]]
链接就是原型链
__proto__
引用了 [[Prototype]]
,可以通过 __proto__.__proto__...
来遍历原型链
所有普通的 [[Prototype]]
链最终都会指向内置的 Object.prototype
var obj = {}
Object.__proto__ === Object.prototype // true
Object.getPrototypeOf(obj) === Object.prototype // true
Object.getPrototypeOf(Object.prototype) // null
Object.getPrototypeOf(Function.prototype) === Object.prototype // true
当试图引用对象的属性时会触发 [[Get]]
操作,对于默认的 [[Get]]
,第一步是检查对象本身是否具有这个属性,如果无法在对象本身找到,就会继续访问对象的 [[Prototype]]
链
var obj = {
id: 1,
name: 'xx'
}
// Object.create()会创建一个对象并把这个对象的 [[Prototype]] 关联到指定的对象
var obj2 = Object.create(obj)
obj2.id // 1
obj2.name // 'xx'
以上例子,创建了一个关联到 obj 的对象 obj2,obj2 的 [[Prototype]]
指向 obj,obj2 本身是个空对象,访问 obj2.id 在 obj2 本身没有找到 id 这个属性,访问到了 [[Prototype]]
找到了这个属性
属性设置和屏蔽
var obj = {
a: 1
}
var _obj = Object.create(obj)
obj.b = 2
// obj上没有b这个属性,则添加b,此时obj -> { a: 1, b: 2 }
_obj.a = 100
// _obj -> { a: 100 },obj -> { a: 1, b: 2 } 此为上图的1
Object.defineProperty(obj, 'b', {
writable: false
})
// 将obj中的b设置为只读
_obj.b = 300
// _obj -> { a: 100 },此时不会在_obj上创建属性b
Object.defineProperty(_obj, 'b', { value: 200 })
// _obj -> { a: 100, b: 200 }
只读属性会阻止 [[Prototype]]
链下层隐式创建同名属性,这样做主要是为了模拟类属性的继承。但实际上并不会发生类似的继承复制,而且这个限制只存在于=赋值中,使 Object.defineProperty() 并不会受到影响
JS中的‘类’
function Foo() {
...
}
var a = new Foo()
Object.getPrototypeOf(a) === Foo.prototype; // true
a.constructor == Foo // true
调用 new Foo() 时会创建 a,其中的一步就是给 a 一个内部的 [[Prototype]]
链接,关联到 Foo.prototype 指向的那个对象
- js 中只有对象
- js 中没有复制机制
- 函数本身并不是构造函数,js 中的‘构造函数’可以理解为
带 new 的函数调用
ES6 的 Class
ES6 的 Class 只是一个语法糖,是 ES5 的构造函数的一层包装
// es5
function es5Foo(x, y) {
this.x = x;
this.y = y;
}
es5Foo.prototype.add = function() {
return this.x + this.y;
}
var f1 = new es5Foo(1, 2);
// es6
class es6Foo {
constructor(x, y) {
this.x = x;
this.y = y;
}
add() {
return this.x + this.y;
}
}
var f2 = new es6Foo(1, 2);
类的所有方法都定义在类的 prototype 上,prototype 的 constructor 指向类本身,这与 ES5 一致
es5Foo.prototype.constructor === es5Foo // true
Object.getPrototypeOf(f1) === es5Foo.prototype // true
es6Foo.prototype.constructor === es6Foo // true
Object.getPrototypeOf(f2) === es6Foo.prototype // true
类的内部所有定义的方法都是不可枚举的,这与 ES5 不同
Object.keys(es5Foo.prototype) // ['add']
Object.keys(es6Foo.prototype) // []
new.target: ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数,如果构造函数不是通过 new 命令或 Reflect.construct() 调用的, new.target 会返回 undefined
- 可以用来确保构造函数只能通过 new 调用
- 可以用来写不能单独使用、必须继承后才能用的类
原型继承
js 中并没有复制机制,new 操作不是创建一个类的多个实例,而是可以创建多个对象并把它们的[[Prototype]]
关联到同一个对象,这个机制就是 js 中的原型继承,通过让一个对象的原型指向另一个对象实现继承
继承方式:
- 原型链继承
- 构造函数继承
- 组合继承
- 原型式继承
- 寄生组合式继承
闭包
一个函数中嵌套一个内部函数,这个内部函数就形成了一个闭包,闭包可以在一个内层函数中访问到外层函数的作用域
function func() {
var local = '123'; // 在函数外部是访问不到的
function innerF() { // 内部函数形成一个闭包
console.log(local)
}
return innerF;
}
var f = func();
f(); // 123
// 经典循环问题
for( var i = 0; i < 5; i++ ) {
setTimeout(() => {
console.log( i );
}, 1000)
} // 输出5个5
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
},1000)
})(i)
} // 0 1 2 3 4
闭包的作用:创建私有变量,延长变量的生命周期
实例:
var makeCounter = function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
},
};
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
两个计数器 Counter1 和 Counter2 维护它们各自的独立性,每个闭包都是引用自己词法作用域内的变量 privateCounter,不会相互影响
事件循环
事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
js 是单线程的,所有的任务需要排队,前一个任务结束,才会执行下一个任务。
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程外有一个任务队列
- 主线程的所有同步任务执行完成就会读取任务队列,异步任务进入主线程开始执行
- 主线程不断重复以上三步就是事件循环机制
同步任务:即主线程上的任务,按照顺序由上⾄下依次执⾏,当前⼀个任务执⾏完毕后,才能执⾏下⼀个任务。
异步任务:不进⼊主线程,⽽是进⼊任务队列的任务,执行完毕之后会产生一个回调函数,并且通知主线程。当主线程上的任务执行完后,就会调取最早通知自己的回调函数,使其进入主线程中执行。
异步任务分为宏任务、微任务
宏任务:
(),setTimeout(),宏任务是宿主发起的,宏任务队列有多个
微任务:new Promise(),new MutationObserve(),微任务是 js 本身发起的,微任务队列只有一个
当前执行栈执行完毕后会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
异步编程
js 是单线程的,一次只能执行一个任务,同步执行效率低,异步主要解决了同步阻塞的问题
回调
回调函数是一段以参数形式传递给其他代码的可执行代码。
一般函数编写方和调用方都是我们自己,但是回调函数编写方是我们自己,但是调用方不是我们,而是我们引用的其他模块即第三方库,我们调用第三方库中的函数,并把回调函数传递给第三方,第三方中的函数调用我们编写的回调函数。第三方库并不清楚后续的具体实现,只能对外提供一个回调函数。
同步回调:主程序等待回调函数的完成
异步回调:主程序和回调函数同时运行,主程序和回调函数的执行位于不同的线程或进程中
假设一个任务需要调用多个服务,每一个服务都依赖于上一个服务的结果,采用异步回调就会形成回调地狱
// 同步
a = GetServiceA();
b = GetServiceB(a);
c = GetServiceC(b);
d = GetServiceD(c);
// 异步
GetServiceA(function(a){
GetServiceB(a, function(b){
GetServiceC(b, function(c){
GetServiceD(c, function(d) {
....
});
});
});
});
Promise
Promise 是异步编程的一种解决方案
Promise有三种状态:
- pending
- fulfilled
- rejected
只有 pending->fulfilled 和 pending->rejected 的状态改变。只要处于fulfilled和rejected,状态就不会再变,即resolved
Promise一旦创建就无法取消,但可以中断后续的链式调用。当Promise链中抛出一个错误时,错误信息沿着链路向后传递,直至被捕获。利用这个特性能跳过链中函数的调用,直至链路终点,变相地结束Promise链
Promise.resolve().then(()=>{
console.log('fulfilled1');
throw 'throw error';
}).then(()=>{
console.log('fulfilled2 这里不会打印');
}).catch(err=>{
console.log('catch err',err);
})
但是,若链路中也对错误进行了捕获,则后续的函数会继续执行
Promise.resolve().then(()=>{
console.log('fulfilled1');
throw 'throw error';
}).then(()=>{
console.log('fulfilled2 这里不会打印');
},err=>{
console.log('rejected2',err) // 链中捕获了错误
}).then(()=>{
console.log('fulfilled3') // 此处会打印
}).catch(err=>{
console.log('catch err',err) // 此处捕获不到错误
})
并发异步操作:
- Promise.all():如果数组中的某个 Promise 被拒绝,Promise.all() 就会立即拒绝返回的 Promise,并终止其他操作
- Promise.allSettled():等待所有输入的 Promise 完成,不管其中是否有 Promise 被拒绝
- Promise.any():返回第一个兑现的 Promise,不会等待其他 Promise 完成
- Promise.race():返回第一个敲定的 Promise,无论兑现还是拒绝
手写 Promise.all
function promiseAll(promises) {
return new Promise((resolve, reject) => {
let arr = []; // 存放各promise的结果
let counter = 0; // 计数器
promises.forEach((item, i) => {
Promise.resolve(item).then(res => {
arr[i] = res;
// 因为promise是异步的,用计数器保证每个promise都完成后再返回结果
counter++;
if(counter == promises.length) {
resolve(arr);
}
}).catch(reject)
})
})
}
Generator
function*
这种声明方式会定义一个生成器函数,它返回一个 Generator对象
yield关键字用于暂停和恢复生成器函数
function* generator(i) {
yield i;
yield i + 10;
}
const gen = generator(10);
console.log(gen.next().value);
// Expected output: 10
console.log(gen.next().value);
// Expected output: 20
async/await
async/await 是ES2017(ES8) 提出的基于 Promise 的解决异步的最终方案
- 使用 async 声明一个函数时,该函数自动返回一个 Promise 对象
- 当遇到 await 时, async 函数会暂停执行,等待 Promise 的结果
- 等待期间,剩余操作会被挂起,被安排在微任务队列中等待处理
- 当 Promise 被解决或拒绝时,async 函数恢复执行,执行栈为空时,事件循环检查微任务队列,并执行其中的任务