javascript 面试题(持续更新)

数据类型相关

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
// 经典循环问题
forvar 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 函数恢复执行,执行栈为空时,事件循环检查微任务队列,并执行其中的任务