Bootstrap

Web前端面试题汇总2--JS篇

JS

Object.create

根据给定对象,创建以该对象为原型的新对象

// 简单模拟实现Object.create
function create(proto) {
	const Noop = function() {};
	Noop.prototype = proto;
	return new Noop();
}

new

new语句执行过程做了以下事情:

  • 创建一个新对象;
  • 该对象会执行[[Prototype]](即__proto__)链接;
  • 将构造函数的作用域赋给新对象(this指向该新对象);
  • 执行构造函数中的代码(给该新对象添加属性、方法);
  • 若无显式返回对象或函数,才返回新对象。
// 模拟实现new1:
function createInstance(Constructor, ...args) {
	const instance = Object.create(Constructor.prototype);
	Constructor.call(instance, ...args);
	return instance;
}

// 模拟实现new2:
function createInstance() {
	const obj = new Object();
	const Constructor = [].shift.call(arguments);
	obj.__proto__ = Constructor.prototype;
	const result = Constructor.apply(obj, arguments);
	return typeof result === 'object' ? result : obj;
}

原型与原型链

prototype的定义
给其他对象提供共享属性的对象。每个函数对象(只有函数对象才有该属性)都有一个prototype属性,此属性指向一个对象(prototype自己也是对象,其实prototype描述的是两个对象之间的某种关系),该对象就是调用构造函数而创建的实例的原型。

__proto__的定义
每个js对象(除null外),都具有__proto__属性,该属性指向该对象的原型(构造函数的原型上不存在此属性)。此属性是来自Object.prototype,与其说其是一个属性,倒不如说是一个getter/setter

constructor的定义
每个原型都有一个constructor属性指向与之关联的构造函数。它是本身或者继承而来的。此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。

普通对象和函数对象
凡通过new Function()创建的对象都是函数对象,其他的都是普通对象(Function、Object也是通过new Function()创建的)。

function Person() {}
const person = new Person();

person.__proto__ === Person.prototype; // true

// Person为构造函数
Person.__proto__ === Function.prototype; // true

// Person.prototype为一个对象(prototype自身也是一个对象)
Person.prototype.__proto__ === Object.prototype; // true

// Object为构造函数(也由new Function()创建而来的)
Object.__proto__ === Function.prototype; // true

// 为原型链的顶端
Object.prototype.__proto__ === null; // true

// Function.prototype为一个对象(prototype自身也是一个对象)
Function.prototype.__proto__ === Object.prototype; // true

原型:每个js对象(除null外),在创建的时候都会与之关联另一个对象,此对象就是我们所说的原型,每个对象都会从原型“继承”属性。

原型比较少人知道的特性

  • ES3时代只有访问属性的get操作才能触发对原型链的查找;ES5新增访问器属性的概念,定义属性getter/setter操作;
  • 普通对象的__proto__属性,其实是在原型链查找出来的,定义在Object.prototype对象上的。

继承

原型继承

  • 将公用方法和属性添加到父级的原型上面;
  • 将子级的原型替换成父级的实例对象。

// 优点:子类可以访问到父类原型上共享的属性和方法;
// 缺点:1.子类修改共享的引用属性会导致父类的也给修改了;
//       2.子类实例时无法向父类传递参数

// 父类
function Person(name, age) {
	this.name = name;
    this.age = age;
}
Person.prototype.setName = function(name) {
    this.name = name;
}
Person.prototype.getName = () {
    return this.name;
}

// 子类
function Student(name, age, stuClass) {
	this.name = name;
    this.age = age;
    this.stuClass = stuClass;
}
// 将子类的原型换成父类的实例对象--就能访问到共享的父类实例的原型了
Student.prototype = new Person();

构造函数继承

function SuperPerson(name, age) {
	this.name = name;
	this.age = age;
}
SuperPerson.prototype.getName = function() {
	return this.name;
}

function subStudent(name, age, stuClass) {
	this.stuClass = stuClass;
    SuperPerson.call(this, name, age);
}

组合继承
原型链继承+构造函数继承

原型式继承
Object.create()的实现方式

function SuperPerson(SubStudent) {
	const Noop = function() {};
	Noop.prototype = SubStudent;
	return new Noop();
}

寄生式继承

function SuperPerson(SubStudent) {
	const instance = Object.create(SubStudent.prototype);
	// 原型上添加属性和方法
	// ...
	return instance;
}

寄生式组合继承

function inheritPrototype(SuperParent, SubChildren) {
	const instance = Object.create(SuperParent.prototype);
	instance.constructor = SubChildren;
	SubChildren.prototype = instance;
}

instanceof

  • 判断一个实例是否属于某种类型;
  • 判断一个实例是否是其父类或祖先类的实例。
function createInstanceOf(children, parent) {
	while(true) {
		if (children.__proto__ === null) { // 原型链顶层
			return false
		} else if (children.__proto__ === parent.prototype) {
			return true
		}
	}
}

function Foo() { }
const foo = new Foo()
console.log(createInstanceOf(foo, Foo));

typeof

底层判断是用机器码进行判断的。

  • 对象:000;
  • 浮点型:010;
  • 字符串:100;
  • 布尔类型:110;
  • 整数:1
    因为null的机器码为000typeof判断时当作对象。

闭包

  • 能够访问自由变量的函数
  • 自由变量是只在函数中使用,但既不是函数参数也不是函数的局部变量的变量;
  • 闭包 = 函数 + 函数能够访问自由变量。

bind

bind方法会创建一个新函数,当此函数被调用时,bind的第一个参数将作为它运行的this,之后一系列参数将会在传递实参前传入作为它的参数。

Function.prototype.bind2 = function(context) {
	// 若传递进来的不是function,抛出错误
	if (typeof this !== 'function') throw new TypeError(this + "must be function")
	const self = this;
	// 剔除context的其他参数,转为数组
	const args = [].slice.call(arguments, 1);
	const bound = function () {
		const boundArgs = [].slice.call(arguments);
		const finalArgs = args.concat(boundArgs);
		// new 调用(其实this instanceof bound判断并不是很准确,ES6的new.target可以解决此问题)
		if (this instanceof bound) {
			// 实现new
			// self可能是ES6的箭头函数,没有prototype,所以没有必要再指向做prototype操作
			if (self.prototype) {
				function Noop() {}
				Noop.prototype = self.prototype;
				bound.prototype = new Noop();
			}
			// 生成的新对象会绑定到函数调用的this
			const result = self.apply(this, finalArgs);
			// new若没有显式返回,会返回新对象
			const isObject = result !== null && typeof result === 'object';
			const isFunc = typeof result === 'function';
			if (isObject || isFunc) return result;
			return this;
		} else {
			return self.apply(context, finalArgs);
		}
	}
	return bound;
}

call和apply

call
call方法使用一个指定的this值和若干个指定参数的前提下调用某个函数或方法。

Function.prototype.call2 = function(context) {
	var context = context || window;
	context.func = this;
	var args = [];
	var array = [].slice.call(arguments, 1)
	for (let i = 1; i < array.length; i++) {
		args.push('arguments[' + i + ']');
	}
	var result = eval('context.func(' + args + ')');
	delete context.func;
	return result;
}

apply
apply方法使用一个指定的this和一个指定数组参数的前提下调用某个函数或方法。

Function.prototype.apply2 = function (context, args) {
	var context = context || window;
	context.func = this;
	var result;
	if (args) {
		var array = [];
		for (let i = 0; i < args.length; i++) {
			array.push("args[" + i + ']')
		}
		result = eval("context.func(" + array + ")")
	} else {
		result = context.func();
	}
	delete context.func;
	return result;
}

深拷贝

const reference = ['Set', 'WeakSet', 'Map', 'WeakMap', 'Date', 'RegExp', 'Error'];
const getType = (target) => Object.prototype.toString.call(target).replace(/\[|\]|object /g, '');

// 方案1--通俗写法
function deepClone(target) {
	let result = null; // 存储拷贝后的对象
	const type = getType(target); // 获取目标参数的类型
	if (type === 'Object') { // 为对象--遍历+递归
		for (const key in target) {
			if (Object.hasOwnProperty.call(target, key)) {
				result[key] = deepClone(target[key]);	
			}
		}
	} else if (type === 'Array') { // 为数组--遍历+递归
		target.forEach((el, i) => {
			result[i] = deepClone(el);
		})
	} else if (reference.includes(type)) { // 为定义好的类型
		result = new target.constructor(target);
	} else { // 基本数据类型与function直接赋值
		result = target;
	}
	return result;
}

// 方案2--使用ES7中的属性
function deepClone(target) {
	let result;
	// 利用Object.create(proto[, propertiesObject])创建一个新对象
	// 参数2即使用对象的描述属性作为新对象的属性
	const _target = Object.create(Object.getPrototypeOf(target), Object.getOwnPropertyDescriptors(target));
	// 遍历对象,克隆属性
	// Reflect.ownKeys(object)-->创建一个新数组,此数组的元素即为参数对象属性的key
	for (const key of Reflect.ownKeys(_target)) {
		const value = _target(key);
		const type = getType(_target);
		if (value !== null && typeOf value === 'object') { // 对象或者数组-->递归
			result[key] = deepClone(value);
		} else if (reference.includes(type)) {
			result[key] = new value.constructor(value);
		} else {
			result[key] = value;
		}
	}
	return result;
}

首层拷贝

// 数组中的concat、slice;Object.assign();展开运算符都是首层拷贝
function shallowClone (target) {
  const _target = target.constructor === 'Array' ? [] : {};
  for (const key in target) {
    if (Object.hasOwnProperty.call(target, key)) {
      _target[key] = target[key];
    }
  }
  return _target;
}

iterator迭代器

ES6中的解构赋值、剩余/扩展运算,生成器、for of循环,底层都是iterator迭代器去实现的。
所谓迭代器就是一个具有next()方法的对象,每次调用next()方法都会返回一个结果对象,该结果对象有两属性,value表示当前的值,done表示遍历是否结束。

function createInterator(items) {
	var i = 0;
	return {
		next: function () {
			var done = i >= items.length;
			var value = !done ? items[i++] : undefined;
			return { value, done }
		}
	}
}

Promise对象

async/await语法基础,是js中处理异步的标准形式。
promise3个状态,分别是pending、fulfilled、rejected:

  1. pending状态,promise可以切换到fulfilled或rejected;
  2. fulfilled状态,不能迁移到其他状态,必须有个不可变的value;
  3. rejected状态,不能迁移到其他状态,必须有个不可变的reason。
/*
 步骤1:new Promise((resolve, reject) => {})
 一个函数参数,包含两个参数
 两个参数分别为函数
*/

/*
 步骤2:promise有3个状态:pending、fulfilled、rejected
 pending(初始化状态):可以将状态迁移成fulfilled或者rejected;
 fufilled(成功状态):状态不可迁移,此时有一个不可变的值value;
 rejected(失败状态):状态不可迁移,此时有一个不可变的reason
*/

/*
 步骤3:then方法,接收两个参数:onFulfilled函数,onRejected函数
*/

/*
 步骤4:解决异步问题:当resolve在异步中执行时,then中的state还处于pending
 解决方案:将成功和失败存到数组中,一旦resolve或reject时,调用数组中的每一个成功或失败
*/

/*
 步骤5:解决链式调用
 1.在第一个then中返回一个新的Promise对象(普通值,将普通值传递给下一个then;promise将promise返回的值传递给下一个then);
*/
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

// 链式调用解决函数
// 1.循环引用问题
// 2.结果若是对象或函数时,递归解析promise,否则直接resolve
function resolvePromise(promise, result, resolve, reject) {
	// 循环引用问题
	if (promise === result) return new TypeError('Chaining cycle detected for promise')
	// 为对象或函数
	if (result !== null && (typeof result === 'objected' || typeof result === 'function')) {
		// 防止多次调用
		let called = false
		try {
			// result下的then
			const then = result.then
			// then若为对象,默认是promise
			if (typeof then === 'objected') {
				// 调用then的方法的回调继续解析
				then.call(result, value => {
					// 防止多次调用
					if (called) return
					called = true
					resolvePromise(promise, value, resolve, reject)
				}, reason => {
					// 防止多次调用
					if (called) return
					called = true
					reject(reason)
				})
			} else {
				resolve(result)
			}
		} catch (error) {
			// 防止多次调用
			if (called) return
			called = true
			reject(error)
		}
	} else { // 直接将普通值resolve出去
		resolve(result)
	}
}

class Promise2 {
	constructor (executor) {
		// 初始化状态为pending
		this.state = PENDING
		// 不可变的值value
		this.value = undefined
		// 不可变的原因reason
		this.reason = undefined

		// 收集成功、失败函数
		this.onFulfilledCallbacks = []
		this.onRejectedCallbacks = []

		// resolve函数
		const resolvedFunc = (value) => {
			this.value = value
			// 执行成功函数--改变状态
			this.state = FULFILLED
			// 执行成功数组中的函数
			this.onFulfilledCallbacks.length && this.onFulfilledCallbacks.forEach(func => func())
		}
		// reject函数
		const rejectedFunc = (reason) => {
			this.reason = reason
			// 执行失败函数--改变状态
			this.state = REJECTED
			// 执行失败数组中的函数
			this.onRejectedCallbacks.length && this.onRejectedCallbacks.forEach(func => func())
		}
		
		// 执行executor函数
		// 使用trycatch防止出错
		try {
			executor(resolvedFunc, rejectedFunc)
		} catch (error) {
			rejectedFunc(error)
		}
	}
	// then()
	then(onFulfilled, onRejected) {
		// 兼容onFulfilled、onRejected非函数情况
		onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
		onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }

		const promise = new Promise2((resolve, reject) => {
			// 解决resolve、reject在异步中执行问题
			if (this.state === PENDING) {
				this.onFulfilledCallbacks.push(() => {
					setTimeout(() => {
						try {
							const result = onFulfilled(this.value)
							resolvePromise(promise, result, resolve, reject)
						} catch (error) {
							reject(error)
						}
					}, 0)
				})
				this.onRejectedCallbacks.push(() => {
					setTimeout(() => {
						try {
							const result = onRejected(this.reason)
							resolvePromise(promise, result, resolve, reject)
						} catch (error) {
							reject(error)
						}
					}, 0)
				})
			}
			if (this.state === FUFILLED) {
				setTimeout(() => {
					try {
						const result = onFulfilled(this.value)
						resolvePromise(promise, result, resolve, reject)
					} catch (error) {
						reject(error)
					}
				}, 0)
			}
			if (this.state === REJECTED) {
				setTimeout(() => {
					try {
						const result = onRejected(this.reason)
						resolvePromise(promise, result, resolve, reject)
					} catch (error) {
						reject(error)
					}
				}, 0)
			}
		})
		return promise
	}
	// catch()
	catch(func) {
		return this.then(null, func)
	}
	// 静态方法resolve
	static resolve(value) {
		return new Promise(resolve => resolve(value))
	}
	// 静态方法reject
	static reject(reason) {
		return new Promise((_, reject)=> reject(reason))
	}
	// 静态方法race
	static race(promises) {
		if (typeof promises !== 'Array') return new TypeError('race arguments must be Array')
		return new Promise2((resolve, reject)=> {
			for (let i = 0, length = promises.length; i < length; i++) {
				promises[i].then(resolve, reject)
			}
		})
	}
	// 静态方法all
	static all(promises) {
		if (typeof promises !== 'Array') return new TypeError('all arguments must be Array')
		const resultArray = []
		let i = 0
		const processData = (data, index, resolve) => {
			resultArray[index] = data
			i++
			if (i === promises.length) resolve(resultArray)
		}
		return new Promise2((resolve, reject) => {
			for (let i = 0, length = promises.length; i < length; i++) {
				promises[i].then(value => {
					processData(value, i, resolve)
				}, reject)
			}
		})
	}
}

隐式转换介绍

在js中,当运算符在运算时,若两边数据不统一,CPU就无法计算。此时编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再计算。由编译器自动转换的方式称为隐式转换。
隐式转换规则

  • 转成String类型:+(字符串连接符);
  • 转成Number类型:++、–(自增自减运算符)、+、-、*、/、%(算术运算符)、>、<、>=、<=、==、!=(关系运算符);
  • 转成Boolean类型:!(逻辑非运算符)。

以下8中情况转换为布尔类型会得到false
0、-0、NaN、undefined、null、空字符换、false、document.all()。除以上8中情况之外都会得到true。

// + 是字符换连接符:String(1) + 'true' = '1true'
console.log(1 + 'true');

// + 是算术运算符:1 + Number(true) = 1 + 1 = 2
console.log(1 + true);

// + 是算术运算符:1 + Number(undefined) = 1 + NaN = NaN
console.log(1 + undefined);

// + 是算术运算符:1 + Number(null) = 1 + 0 = 1
console.log(1 + null);

// 关系运算符两边有一边是字符串的时候,会将其他数据类型使用Number()转换,然后比较。
// Number('2') > 10 = 2 > 10 = false
console.log('2' > 10);

// 关系运算符两边都是字符串时,此时同时转成number然后比较关系。
// 注意:此时并不是按照Number()的形式转成数字,而是按照字符串对应的unicode编码来转成数字
// '2'.charCodeAr() > '10'.charCodeAt() = 50 > 49 = true
console.log('2' > '10');

// 多个字符从左到右依次比较
// 先比较'a'、'b','a'与'b'不等,则直接出结果(a的unicode码比b的unicode码小)
console.log('abc' > 'b'); // false

// 先比较'a'和'a',两者相等,继续比较第二个字符'a'和'b',得出结果
console.log('abc', 'aad'); // true

// 特殊情况(无视规则):若数据类型是undefined、null,得出固定结果
console.log(undefined == undefined); // true
console.log(undefined == null); // true
console.log(null == null); // true
// 特殊情况(无视规则):NaN与任何数据比较(包括其本身)都是NaN
console.loe(NaN == NaN); // false

复杂数据类型在隐式转换时会先转成String,然后再转成Number运算
在这里插入图片描述

var a = ??
if (a == 1 && a == 2 && a == 3) {
	console.log(1);
}
// 如何完善a,使其正确打印1

// 因为对象的valueOf()可以被重写,重写a对象中的valueOf()使条件成立
var a = {
	i: 0, // 声明一个变量属性
	valueOf: function() {
		// 每调用一次,让对象a的i属性自增一次并返回
		return ++a.i;
	}
}
if (a == 1 && a == 2 && a == 3) { // 每一次运算时都会调用一次valueOf()
	console.log(1);
}

// 复杂数据类型转number顺序:
// 1.先使用valueOf()方法获取其原始值,若原始值不是number类型,则使用toString()方法转成String;
// 2.再将string转成number运算。

// 先将左边数组转成string,然后右边也是string,则转成Unicode编码运算。
// [1,2].valueOf() = [1,2],非number类型,使用toString()
// [1,2].valueOf().toString() = '1,2'
console.log([1, 2] == '1,2'); // true

// a.valueOf().toString()  = '[object Object]'
var a = {};
console.log(a == '[object Object]'); // true

// [].valueOf().toString() = ''
// Number('') = 0;
console.log([] == 0); // true

// 逻辑非优先级高于关系运算符
// ![] = false([]转为布尔类型为true(布尔类型除8种情况以外都为true))
// false == 0;
cosole.log(![] == 0); // true

// 逻辑非优先级高于关系运算符
// ![] = false([]转为布尔类型为true(布尔类型除8种情况以外都为true))
// [] == false
// 根据隐式转换规则(==:转为number类型比较)
// [].valueOf().toString() = ''
// Number('') == Number(false)
console.log([] == ![]); // true

//引用类型存储在堆中,栈中存储的是地址,所以结果是false
console.log([] == []); // false

// 逻辑非优先级高于关系运算符
// !{} = false({}转为布尔类型为true(布尔类型除8种情况以外都为true))
// 根据隐式转换规则(==:转为number类型比较)
// {}.valueOf().toString() = '[object Object]'
// Number('[object Object]') == Number(false)
console.log({} == !{}); // false

//引用类型存储在堆中,栈中存储的是地址,所以结果是false
console.log({} == {}); // false

proxy的理解

Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。

var target = {
   name: 'zhangsan',
   age:20,
   sex:'男'
 }
var logHandler = {
  get(target, key) {
    console.log(`${key}被读取`)
    return target[key]
   },
  set(target, key, value) {
    console.log(`${key}被设置为${value}`)
    target[key] = value
  }
}
var demo = new Proxy(target, logHandler)
demo.name  //name被读取

JavaScript中的数组和函数在内存中如何存储

  • 同种类型数据的数组分配连续的内存空间;
  • 存在非同种类型数据的数组使用哈希映射分配内存空间。

温馨提示:连续的内存空间只需要根据索引(指针)直接计算存储位置即可。哈希映射需要计算索引值,然后索引值有冲突的场景下还需要二次查找(需要知道哈希的存储方式)

Object.defineProperty有哪几个参数,各自都有什么作用

可以给一个对象添加属性以及这个属性的属性描述符/访问器(这2个不能共存,同一属性只能有其中一个),属性描述符有configurable,writable,enumerable,value这4个属性,分别代表是否可配置,是否只读,是否可枚举和属性的值,访问器有configurable,enumerable,get,set,前2个和属性描述符功能相同,后2个都是函数,定义了get,set后对元素的读写操作都会执行后面的getter/setter函数,并且覆盖默认的读写行为。

Object.defindProperty有三个参数:

  1. 对象;
  2. 对象的属性
  3. 属性描述符

Object.defineProperty和ES6的Proxy的区别

Proxy的优势

  • Proxy可以直接监听对象而非属性;
  • Proxy可以直接监听数组的变化:
    1.Proxy有多大13种拦截方法,不限于apply、ownKeys、deleteProperty、has等,是Object.definedProperty不具备的;
    2.Proxy返回的是一个新对象,可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;
    3.Proxy作为新标准将受到浏览器厂商重新持续的性能优化(即传说中的新标准的性能红利);

Object.defineProperty的优势:

  • 兼容性能好,支持IE9,而Proxy存在浏览器兼容性问题,且无法使用polyfill磨平。

宏任务和微任务

在异步模式下,创建异步任务主要分为宏任务和微任务两种。ES6规范中,宏任务(Macrotask)称为Task;微任务(Microtask)称为Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由js自身发起的。

宏任务(Macrotask)微任务(Microtask)
setTimeoutrequestAnimtionFrame(有争议)
setIntervalMutationObserver(浏览器)
MessageChannelPromise.[ then/catch/finally ]
I/O,事件队列process.nextTick(Node环境)
setImmediate(Node环境)queueMicrotask
script(整体代码块)

如何理解script(整体代码块)是宏任务:
实际上若同时存在两个script代码块,会首先执行第一个script代码块中的同步代码。若此过程中创建了微任务并进入了微任务队列,第一个script同步代码执行完之后,会首先去清空微任务队列,再去开启第二个script代码块的执行。

EventLoop

在这里插入图片描述
js引擎遇到异步事件之后,会将此事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果之后,将此事件加入到当前执行栈的另一队列中(即事件队列)。被放入事件队列中并不会立即执行回调,而是等待当前执行栈中的所有同步任务执行完毕,主线程处于闲置状态下,主线程会去查找事件队列中是否有任务。若有的话,主线程会从当中取出在第一位的事件,并将此事件对应的回调加入到执行栈中,然后执行其他同步代码。如此反复,形成了无限循环(即事件循环(EventLoop))。
在这里插入图片描述

;