javascript 基础

148 阅读14分钟

面试复习知识点参考链接:segmentfault.com/a/119000002…

执行上下文

  1. 类型
  • 全局执行上下文:默认的上下文,创建全局的window对象,设置this指向该全局对象。一个程序中只有一个全局执行上下文。
  • 函数执行上下:函数被调用时,会创建函数执行上下文。可以有多个函数执行上下文。
  • Eval函数执行上下文(不常用)。
  1. 如何创建
  • 创建阶段:this绑定;词法环境(存储函数声明和变量绑定let、const);变量环境(存储var变量绑定)。
  • 执行阶段:分配完变量,执行代码。

作用域链

  1. 定义
  • 作用域是程序代码中规定变量的区域,规定了如何查找变量,确定当前代码对变量的访问权限。
  • 作用域链是由多个执行上下文的变量对象构成的链表。查找变量时先从当前变量对象查找,若没找到,再一层一层往父级中找,直到找到全局对象。
  1. 如何创建
  • 函数创建(确定了静态作用域)、函数激活(确定动态作用域)

闭包

  1. 定义
  • 闭包就是能访问到外部作用域的内部函数,不论外部作用域是否执行结束。
  1. 形成条件
  • 函数嵌套
  • 内部函数引用了外部函数的局部变量
  1. 优缺点
  • 优点:避免变量污染;读取函数内部变量;在内存中维护变量;利于代码封装
  • 缺点:常驻内存,增大内存使用量,使用不当容易造成内存泄露(解决方案:退出函数前将不用的局部变量全部删除)
  1. 使用场景:柯里化(将传入一个多入参的函数封装为嵌套的只传一个入参的函数)、模块化
  • 定时器传参
  • 回调等事件处理、AJAX接口请求等异步任务
  • 封装变量
  1. 闭包何时回收
  • 全局变量引用闭包时,闭包会一直保存在内存中,直到页面关闭
  • 局部变量引用闭包时,内部函数执行结束后会立即销毁,下次js执行垃圾回收时判断是否使用再决定是否销毁闭包并回收内存

this的指向

  1. this在全局上下文中指向全局对象,若要判断函数的this指向,需找到函数被调用的位置。
  • 直接调用:默认指向全局变量
  • call()、apply()、bind():指向被绑定的对象
  • 箭头函数:this指向外层,箭头函数没有自己的this
  • 作为对象的一个方法:指向调用函数的对象
  • 作为构造函数:指向正在构造的新对象
  • 作为DOM事件处理函数:指向触发事件的元素,即始事件处理程序坐绑定的DOM节点
  • HTML标签内联事件处理函数、jQuery的this:指向所在的DOM元素
  1. 如何改变this的指向
  • 使用call()、apply()、bind()
  • 存储this指向到变量中,如 var _this=this
  • 使用箭头函数

call、apply、bind

  1. call:传递的参数需逐个列出来
  • 使用:Function.call(obj, param1, param2)
// 手动实现call()
Function.prototype.call = function (obj) {
  obj = obj ? Object(obj) : window;
  obj.fn = this;
  // 利用拓展运算符直接将arguments转为数组
  let args = [...arguments].slice(1);
  let result = obj.fn(...args);

  delete obj.fn;
  return result;
};
  1. apply:传递的是参数数组
  • 使用:Function.apply(obj, [param1, param2])
// 手动实现apply()
Function.prototype.apply = function (obj, arr) {
  obj = obj ? Object(obj) : window;
  obj.fn = this;
  let result;
  if (!arr) {
    result = obj.fn();
  } else {
    result = obj.fn(...arr);
  }
  delete obj.fn;
  return result;
};
  1. bind:返回新函数、新函数的this无法再次更改;bind不是立即执行,被再次调用时才会执行
  • 使用:Function.bind(obj, param1)
Function.prototype.bind = function (obj) {
  if (typeof this !== "function") {
    throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  }
  let args = Array.prototype.slice.call(arguments, 1);
  let fn = this;
  //创建中介函数
  let fn_ = function () {};
  let bound = function () {
    let params = Array.prototype.slice.call(arguments);
    //通过constructor判断调用方式,为true this指向实例,否则为obj
    fn.apply(this.constructor === fn ? this : obj, args.concat(params));
    console.log(this);
  };
  fn_.prototype = fn.prototype;
  bound.prototype = new fn_();
  return bound;
};

Promise、async/await

  1. Promise(ES6)
  • Promise是包含then方法的对象或函数,它将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。Promise的状态必须为其中一种:Pending(等待态)、Fulfilled(执行态)、Rejected(拒绝态)。且状态只能由pending变为resolved或rejected,变化后不可再改变。
  • 使用Promise对象进行链式编程(,保证代码的执行顺序;then方法核心用途是构造下一个promise的result,用来接收处理成功时的响应数据,catch接收失败时的数据,finally用于处理正常或异步后的最后一次处理。最大的问题就是代码冗余,因为.then太多,不利于代码维护。
  • 链式Promise是指在当前promise达到fullfilled状态后,会执行其回调函数,回调函数返回的结果被当作value返回给下一个Promise(即then中产生的Promise),以此类推,链式调用的效应就出来了。
  • 事件循环中先执行宏任务再执行微任务。
  • 类方法:Promise.resolve()Promise.reject()Promise.race()Promise.all()Promise.allSettled()
  • 原型方法:Promise.prototype.then(resolved, rejected)Promise.prototype.catch(rejected)Promise.prototype.finally()
  • 测试题:juejin.cn/post/684490…
// Promise()
function fn() {
  return new Promise((resolve, reject) => {
    // 成功时调用 resolve(数据)
    // 失败时调用 reject(错误)
  });
}
fn().then(success1, fail1).then(success2, fail2).catch(fail);

// Promise.all() - 所有的promise都调用成功后才会调用success
Promise.all([promise1, promise2]).then(success, fail)

// Promise.race() - 只要有其中一个promise调用成功就会调用success
Promise.race([promise1, promise2]).then(success, fail)
  1. async/await(ES7)
  • async/await语法糖。async表示该函数为异步函数,不会阻塞后面函数的执行,async函数返回数据时自动封装为一个Promise对象,处理成功时用then接收,失败时用catch接收。
  • 当代码执行到async函数中的await时,代码在此处等待不继续往下执行,直到await拿到Promise对象中resolve数据后,才继续往下执行,这样保证了代码的执行顺序,使异步代码看起来更像同步代码。
    • await只能在使用async定义的函数中使用,不能单独使用
    • await可以直接拿到Promise中resolve中的数据
    • await后面可以跟任何表达式,更多的是跟一个返回Promise对象的表达式
  1. 如何解决回调地狱
  • 使用Promise对象进行链式编程
  • 使用async/await语法糖
  1. 如何实现一个通过Promise/A+规范的Promise
function Promise(fn) {
	let state = "pending";
	let value = null;
	const callbacks = [];

	this.then = function (onFulfilled, onRejected) {
		return new Promise((resolve, reject) => {
			handle({
				onFulfilled,
				onRejected,
				resolve,
				reject,
			});
		});
	};

	this.catch = function (onError) {
		this.then(null, onError);
	};

	this.finally = function (onDone) {
		this.then(onDone, onError);
	};

	this.resolve = function (value) {
		if (value && value instanceof Promise) {
			return value;
		}
		if (
			value &&
			typeof value === "object" &&
			typeof value.then === "function"
		) {
			const { then } = value;
			return new Promise((resolve) => {
				then(resolve);
			});
		}
		if (value) {
			return new Promise((resolve) => resolve(value));
		}
		return new Promise((resolve) => resolve());
	};

	this.reject = function (value) {
		return new Promise((resolve, reject) => {
			reject(value);
		});
	};

	this.all = function (arr) {
		const args = Array.prototype.slice.call(arr);
		return new Promise((resolve, reject) => {
			if (args.length === 0) return resolve([]);
			let remaining = args.length;

			function res(i, val) {
				try {
					if (val && (typeof val === "object" || typeof val === "function")) {
						const { then } = val;
						if (typeof then === "function") {
							then.call(
								val,
								(val) => {
									res(i, val);
								},
								reject
							);
							return;
						}
					}
					args[i] = val;
					if (remaining === 0) {
						resolve(args);
					}
				} catch (ex) {
					reject(ex);
				}
			}
			for (let i = 0; i < args.length; i++) {
				res(i, args[i]);
			}
		});
	};
	this.race = function (values) {
		return new Promise((resolve, reject) => {
			for (let i = 0, len = values.length; i++; ) {
				values[i].then(resolve, reject);
			}
		});
	};
	function handle(callback) {
		if (state === "pending") {
			callbacks.push(callback);
			return;
		}
		const cb =
			state === "fulfilled" ? callback.onFulfilled : callback.onRejected;
		const next = state === "fulfilled" ? callback.resolve : callback.reject;
		if (!cb) {
			next(value);
			return;
		}
		try {
			const ret = cb(value);
			next(ret);
		} catch (e) {
			callback.reject(e);
		}
	}
	function resolve(newValue) {
		const fn = () => {
			if (state !== "pending") return;
			if (
				newValue &&
				(typeof newValue === "object" || typeof newValue === "function")
			) {
				const { then } = newValue;
				if (typeof then === "function") {
					// newValue 为新产生的 Promise,此时resolve为上个 promise 的resolve
					// 相当于调用了新产生 Promise 的then方法,注入了上个 promise 的resolve 为其回调
					then.call(newValue, resolve, reject);
					return;
				}
			}
			state = "fulfilled";
			value = newValue;
			handelCb();
		};
		setTimeout(fn, 0);
	}
	function reject(error) {
		const fn = () => {
			if (state !== "pending") return;
			if (error && (typeof error === "object" || typeof error === "function")) {
				const { then } = error;
				if (typeof then === "function") {
					then.call(error, resolve, reject);
					return;
				}
			}
			state = "rejected";
			value = error;
			handelCb();
		};
		setTimeout(fn, 0);
	}
	function handelCb() {
		while (callbacks.length) {
			const fn = callbacks.shift();
			handle(fn);
		}
	}
	fn(resolve, reject);
}

深浅拷贝

  1. 浅拷贝:A变B变
  • 复制基本类型的数据或指向某对象的指针,不是复制对象本身,源对象跟目标对象共享同一内存。
  • 多层数据
    • concat()slice()Object.assign()Object.create()
    • 对象的解构
  1. 深拷贝:A变B不变
  • 跟浅拷贝相反。完全是克隆一个新对象,不共享内存。实现方法就是递归调用浅拷贝。
  • 一层数据
    • concat()slice()Object.assign()Object.create()
    • 对象的解构
    • JSON.parse(JSON.stringify())(不能深拷贝循环引用)
    • jquery的方法$.extend(deep,target,object1,objectN)($.extend(是否深拷贝,目标对象,即将被合并的对象))
/* deep 为 true 表示深复制,为 false 表示浅复制
 * sourceObj 表示源对象
 * 执行完函数,返回目标对象
 */
function clone(deep = true, sourceObj = {}) {
  let targetObj = Array.isArray(sourceObj) ? [] : {};
  let copy;
  for (var key in sourceObj) {
    copy = sourceObj[key];
    if (deep && typeof copy === "object") {
      if (copy instanceof Object) {
        targetObj[key] = clone(deep, copy);
      } else {
        targetObj[key] = copy;
      }
    } else if (deep && typeof copy === "function") {
      targetObj[key] = eval(copy.toString());
    } else {
      targetObj[key] = copy;
    }
  }
  return targetObj;
}
  1. 如何解决循环引用的问题
  • 父级引用:判断一个对象的字段是否引用了这个对象或这个对象的任意父级。
  • 同级引用
  • 设置一个数组或哈希表用来存储已拷贝过的对象,当检测到对象已存在于哈希表中时,取出值并返回。
  1. 使用时有哪些注意事项
  • 使用JSON.parse(JSON.stringify())实现深拷贝时,构造函数、function、Undefined等丢失,正则表达式变{},Data变String,Set类型、Map类型变Array。

js运行机制、Event Loop、宏任务、微任务

  1. js运行机制
  • 每次主线程执行栈(同步任务)为空的时候,引擎再执行异步任务,异步任务分为宏任务和微任务,在事件轮询中会优先处理微任务,队列处理完微任务队列里的所有任务,再去处理下一宏任务。
  1. 事件循环
  • Event Loop是让js既是单线程、又不会阻塞,用来协调各种事件、用户交互、脚本执行、UI渲染、网络请求等的一种异步机制。可以有多个task(macrotask)、一个microtask队列。
  1. Event Loop的运行机制
  • 执行栈选择最先进入队的宏任务(一般是script),执行其同步代码直至结束
  • 检查是否存在微任务,有的话则执行至微任务队列为空
  • 若宿主为浏览器,可能会渲染页面
  • 开始下一轮事件轮询,执行宏任务中的异步代码 image.png
  1. Event loop相关结论
  • 在一轮Event loop中多次修改统一dom,只有最后一次回进行绘制。
  • UI的渲染更新会在Event loop中的tasks和microtasks完成后进行,并非每轮Event loop都会更新渲染,这取决于浏览器觉得是否有必要实时将新状态呈现给用户;若一帧时间内修改了多处dom,浏览器可能将变动攒起来最后只进行一次绘制。
  • 若希望每轮Event loop都及时呈现变动,可使用requestAnimationFrame
  1. 常见的宏任务微任务
  • 宏任务:由宿主(node、浏览器)发起的,DOM渲染后触发
    • setTimeoutsetIntervalsetImmediate(node.js)、requestAnimationFramepostMessagescriptUI render
  • 微任务:由js引擎发起的,DOM渲染前触发
    • process.nextTick(node.js)、promise.thenasync/awaitObject.observeMutationObserve
  1. 例题
// 思路:第一轮宏任务 -> 第一轮微任务 -> 第二轮宏任务 -> ···
console.log("script start"); // 1
async function async2() {
	console.log("async2 end"); // 2
}
async function async1() {
	await async2();
	console.log("async1 end"); // 5
}
async1();
setTimeout(function() {
	console.log("setTimeout"); // 8
}, 0);
// Promise本身是同步任务,.then执行微任务
new Promise(resolve => {
	console.log("Promise"); // 3
	resolve();
}).then(function() {
	console.log("promise1"); // 6
}).then(function() {
	console.log("promise2"); // 7
});
console.log("script end"); // 4
  1. 实现一个事件订阅-发布
// 定义事件中心类
class MyEvent {
	handlers = {}; // 存放事件 map,发布者,存放订阅者

	$on(type, fn) {
		if (!Reflect.has(this.handlers, type)) {
			// 如果没有定义过该事件,初始化该订阅者列表
			this.handlers[type] = [];
		}
		this.handlers[type].push(fn); // 存放订阅的消息
	}

	$emit(type, ...params) {
		if (!Reflect.has(this.handlers, type)) {
			// 如果没有该事件,抛出错误
			throw new Error(`未注册该事件${type}`);
		}
		this.handlers[type].forEach(fn => {
			// 循环事件列表,执行每一个事件,相当于向订阅者发送消息
			fn(...params);
		});
	}

	$remove(type, fn) {
		if (!Reflect.has(this.handlers, type)) {
			throw new Error(`无效事件${type}`);
		}
		if (!fn) {
			// 如果没有传入方法,表示需要将该类型的所有消息取消订阅
			return Reflect.deleteProperty(this.handlers, type);
		}
		const inx = this.handlers[type].findIndex(handler => handler === fn);
		if (inx === -1) {
			// 如果该事件不在事件列表中,则抛出错误
			throw new Error("无效事件");
		}
		this.handlers[type].splice(inx, 1); // 从事件列表中删除该事件
		if (!this.handlers[type].length) {
			// 如果该类事件列表中没有事件了,则删除该类事件
			return Reflect.deleteProperty(this.handlers, type);
		}
	}
}

原型、原型链、原型实现继承

可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性;使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true。

浏览器

  1. js是单线程,但浏览器内核是多线程,故可以将UI渲染、定时器触发、HTTP请求等工作交给专门的线程来处理
  2. 内核组成:人机交互部分、网络请求部分、js引擎部分(解析并执行js)、渲染引擎部分(渲染html、css)、数据存储部分
  3. 渲染流程:解析html、css,生成布局树,布局计算,生成图层树,绘制,光栅化、生成位图,合成,显示界面
  4. 页面优化
    • 加载阶段原理
      • 减少关键资源个数,减少请求次数
      • 减小关键资源大小,提到加载速度
      • 传输资源需要的往返时间RTT(Round Trip Time)
    • 加载阶段具体优化方法
      • 压缩HTML文件,移除不必要的注释
      • 合并并压缩CSS、JS等文件,script标签加上async或defer属性
      • 使用缓存(第二次请求中若数据有数据则直接读取缓存)
      • 避免使用table布局
    • 更新阶段原理
      • 减少页面渲染过程的重排、重绘
    • 更新阶段具体优化方法
      • 减少DOM操作,将多次操作合并为一次
      • 减少逐项更改样式,最好一次性更改style,或者定义为class一次性更新
      • 避免多次读取offset等属性,使用变量做缓存
      • 防抖、节流
      • 做动画效果时,使用will-change和transform做优化
      • 前端框架Vue、React(虚拟DOM和Diff算法等)
  5. 浏览器安全
    • XSS攻击:跨站脚本攻击
      • 攻击方式
        • 通过document.cookie获取用户的Cookie信息,发送到恶意服务器,模拟用户登录、转账等操作
        • 通过addEventListener来监听用户行为,获取用户信息,发送至恶意服务器
        • 生成广告影响用户体验
      • 解决方法
        • 对输入的脚本进行过滤或转码
        • 响应头Set-Cookie加使用权限
    • CSRF攻击:跨站请求伪造
      • 攻击方式
        • 通过img的src自动跳转到恶意网站
        • 诱导用户点击隐藏链接,指向恶意网站
      • 解决方法
        • 使用Tokenz验证
        • 验证请求来源站点
        • SameSite=Strict,完全禁止此Cookie,不能随着跳转链接跨站发送

常用方法:数组方法、ES6后的

  1. 加粗的方法是能够修改原数组的。 | 方法 | 使用 | 说明 | | :------ | ------ | ------ | | concat | arr.concat(arr1, arr2) | 连接多个数组 | | join | arr.join(",") | 数组转字符串时指定分隔符 | | pop | arr.pop() | 删除数组最后一个元素 | | push | arr.push("str") | 添加 | | reverse | arr.reverse() | 颠倒数组的元素 | | shift | arr.shift() | 删除数组的第一个元素 | | unshift | arr.unshift("str") | 在数组开头添加一个或多个元素 | | slice | arr.slice(startIndex, endIndex) | 从数组中选定从开始索引(包括)到结束索引(不包括)的元素。索引的正负值决定了从数组头部还是尾部算 | | sort | arr.sort((a,b) => {return a-b}) | 数组排序,默认升序 | | splice | arr.splice(index, num, any参数) | 从指定索引index删除num个元素;也可插入新元素 | | reduce | arr.reduce((a,b) => a+b) | 数组累加器,求和 | | includes | arr.includes("str", index) | 数组是否包含指定值,index默认为0 | | indexOf | arr.indexOf("str") | 返回某个指定元素的索引 |

  2. 数组的其它方法

  • 遍历:every()、some()、filter()、map()、find()、forEach()、findIndex()
  • 判断是否是数组:Array.isArray()
  • 浅拷贝新建一个数组:Array.from()
  • 创建数组且具有可变数量的参数:Array.of()
  • 数组去重:var arr2 = [...new Set(arr)]
  1. ES6后的方法
  • sort、Array.isArray、Array.from、Array.of、扩展运算符、includes、flat、flatMap、entries、keys、values、fill、find、findIndex、copyWithin

service worker

web worker

require和import的区别

  1. require/exports - CommonJS
  • 原生浏览器不支持
  • 运行时动态加载,加载的是一个对象,输出得是一个值得拷贝,所以文件引用的模块之改变,require引入的模块值不会改变
  • 先引用后使用
  • 在模块顶层、函数、判断语句等代码块中均可引用
  • require.content(目录,是否还搜索子目录,匹配的正则表达式)
  1. import/export - ES6
  • 在浏览器中无法直接使用,浏览器引入模块的 <script> 元素要添加 type="module 属性
  • 静态编译,输出得是值的引用,所以文件引用的模块值改变,import引入的模块值会改变。可使用import()函数实现动态加载
  • 也可在引用前使用模块
  • 只能在模块顶层使用,不能在函数、判断语句等代码块中引用,因为import在代码静态解析阶段就会生成,不会去分析代码块里面的import

diff算法