备战前端面试—JS基础篇
前言
拿到一道面试题,第一时间应该思考什么?
考点
如何对待做不完的题海?
考点不变,以不变应万变,抛砖引玉,举一反三
如何准备面试?
建立知识体系,由题转到知识点,由知识点反推可能会出现的题目
JS 基础
变量类型和计算
考点1:变量类型
typeof 能判断哪些类型?
- 值类型:number,string,boolean,undefined,symbol 。
- 引用类型( null 是特殊的引用类型,指向空地址)除 function 外无法区分,都为 object。
值类型和引用类型的区别?
值类型:
- 占用空间固定,用栈存储。
- 变量的值就是值本身。
- 使用
typeof
检测数据的类型。
引用类型:
- 占用空间不固定,用堆存储,以便反复利用(因为对象的创建成本通常较大)。
- 变量的值是内存地址,一个内存地址对应一个对象。
- 使用
instanceof
检测数据类型。
浅拷贝?深拷贝?
浅拷贝:
Array.prototype.cocat
。let arr = [1, 2, 3]; let newArr = arr.concat(); newArr[1] = 100; console.log(arr);//[ 1, 2, 3 ]
Object.assign
。let obj = { name: 'sy', age: 18 }; const obj2 = Object.assign({}, obj, {name: 'sss'}); console.log(obj2);//{ name: 'sss', age: 18 }
展开运算符
。let arr = [1, 2, 3]; let newArr = [...arr];//跟arr.slice()是一样的效果
Array.prototype.slice
。let arr = [1, 2, 3]; let newArr = arr.slice(); newArr[0] = 100; console.log(arr);//[1, 2, 3]
深拷贝:
判断是值类型还是引用类型。
判断是数组还是对象。
递归解析。
// 简易版本,还有特殊对象、循环引用、函数类型不能处理。 function deepClone(source) { if (typeof source === "object" && source !== null) { const target = Array.isArray ? [] : {}; for (let key in source) { if (source.hasOwnProperty(key)) { // 保证 key 非原型属性 target[key] = deepClone(source[key]); } } return target; } else { return source; } }
'1'.toString()
为什么可以调用?其实在这个语句运行的过程中做了这样几件事情:
var s = new String('1'); s.toString(); s = null;
第一步: 创建
String
类实例。第二步: 调用实例方法。
第三步: 执行完方法立即销毁这个实例。
整个过程体现了
基本包装类型
的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean
,Number
和String
。instanceof 能否判断基本数据类型?
能。比如下面这种方式:
class PrimitiveNumber { static [Symbol.hasInstance](x) { return typeof x === 'number' } } console.log(111 instanceof PrimitiveNumber) // true
其实就是自定义 instanceof 行为的一种方式,这里将原有的 instanceof 方法重定义,换成了 typeof,因此能够判断基本数据类型。
能不能手动实现一下instanceof的功能?
function myInstanceof(left, right) { //基本数据类型直接返回false if(typeof left !== 'object' || left === null) return false; //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象 let proto = Object.getPrototypeOf(left); while(true) { //查找到尽头,还没找到 if(proto == null) return false; //找到相同的原型对象 if(proto == right.prototype) return true; proto = Object.getPrototypeof(proto); } }
考点2:类型转换
何时使用
==
or===
?- 判断 null 和 undefined 用
==
。 - 其他情况一律
===
。
- 判断 null 和 undefined 用
加减运算?
- 加法运算中只要含字符串,会被解析为字符串拼接,结果为字符串。
- 减法运算中默认转换为数字类型。
if条件判断?
隐式类型转换。
truly变量:两步非运算为true。
falsely变量:两步非运算为false。
// 除此之外,都是 truly 变量 !!0 === false !!NaN === false !!'' === false !!null === false !!undefined === false !!false === false
对象转原始类型是根据什么流程运行的?
对象转原始类型,会调用内置的
[ToPrimitive]
函数,对于该函数而言,其逻辑如下:如果有 Symbol.toPrimitive() 方法,优先调用再返回
调用 valueOf(),如果转换为原始类型,则返回
调用 toString(),如果转换为原始类型,则返回
如果都没有返回原始类型,会报错
var obj = { value: 3, valueOf() { return 4; }, toString() { return "5"; }, [Symbol.toPrimitive]() { return 6; }, }; console.log(obj + 1); // 输出7
如何让if(a == 1 && a == 2)条件成立?
var a = { value: 0, valueOf: function() { this.value++; return this.value; } }; console.log(a == 1 && a == 2);//true
原型和原型链
考点1:继承
es5 和 es6 继承的区别?
es5:
借助原型链和构造函数实现继承。
子类构造函数的原型对象指向父类构造函数的原型对象。
需要修正子类的构造函数指向。
new 时先创建一个空对象将其绑定 this,子类构造函数再以 call、apply 调用父类方法。
function Parent(name) { this.name = name; } Parent.prototype.getName = function () { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); this.age = age; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; Child.prototype.getAge = function () { console.log(this.age); }; var child1 = new Child("a", "1"); child1.getName(); child1.getAge();
es6:
通过关键字 class 定义类,extends 实现继承。
类的 constructor 属性相当于 es5 中的构造函数。
new 时先创建一个空对象,如果是子类,在子类的构造函数中必须先调用 super 方法,通过父类的构造函数将该对象绑定给 this,否则将会报错。
class Parent { constructor(name) { this.name = name; } getName() { console.log(this.name); } } class Child extends Parent { constructor(name, age) { super(name); this.age = age; } getAge() { console.log(this.age); } } var child1 = new Child("a", "1"); child1.getName(); child1.getAge();
能不能描述一下原型链?
JavaScript对象通过
[[Prototype]]
指向父类对象,直到指向Object
对象为止,这样就形成了一个原型指向的链条, 即原型链。在对象上如果找不到某个属性或者方法,就会到它的原型对象上找,这样就实现了对象之间方法和属性的共享。另外:
- 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性。
- 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true。
考点2:显式原型和隐式原型
显式原型和隐式原型是什么?
显式原型:
- 每个函数都有一个
prototype
属性。 - 定义函数时自动添加,默认值是一个空
Object
对象。
隐式原型:
每个实例对象都有一个
__proto__
属性。new
对象时自动添加,默认值为构造函数的prototype
属性值。对象的隐式原型的值为其对应构造函数的显式原型的值。
Object.prototype
的__proto__
属性指向 null。
- 每个函数都有一个
通过 Object.create() 创建出来的对象和 new Object() 字面量创建的对象有什么区别?
Object.create():
使用现有的对象来提供新创建的对象的
__proto__
。var objA = Object.create(Base) // 简易版本 Object.create = function (Base) { var F = function () {}; F.prototype = Base; return new F(); // objA._proto_ = F.prototype = Base};
new Object():
创建的对象是
Object
的实例。var objB = new Base() // 简易版本 var objB = new Object(); objB.__proto__ = Base.prototype; Base.call(objB);
字面量创建:
创建的对象是
Object
的实例。创建更高效一些,少了
__proto__
指向和this
转换。var objC = {};
构造函数与原型对象、实例对象的关系?
- 构造函数的属性
prototype
指向了原型对象。 - 原型对象保存着实例共享的方法,有一个指针
constructor
指回构造函数。 - 调用构造函数产生的实例对象,每个对象都有一个
__proto__
属性,指向这个对象的构造函数的原型对象。
- 构造函数的属性
作用域
考点1:作用域、作用域链
什么是作用域链?
在 es6 之前,只存在两种作用域:函数作用域和全局作用域。当在执行上下文中访问一个变量时,首先会在当前作用域寻找。如果不存在对应的标识符,则会去父作用域找,一直查找到全局作用域。每个函数作用域中都保存着一个外界作用域的引用,这些作用域形成的链条就是作用域链。
这里可适当引出词法环境、执行上下文的概念和作用域的本质。
[[scope]]指向scope chain(ECMA-262v3)或者Lexical Environment(ECMA-262v5)。这对应于闭包的环境部分。
[[scope]]中可访问的属性列表即是标识符列表,对象本身的引用则对应于环境。为什么要引入 let、const?
ES6之前是不支持块级作用域的,因为当初设计这门语言的时候,并没有想到JavaScript会火起来,所以只是按照最简单的方式来设计。没有了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是JavaScript中的变量提升。
为了解决这些问题,ES6引入了let和const关键字,从而使JavaScript也能像其他语言一样拥有块级作用域。
考点2:闭包
- 什么是闭包?
红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。
MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。
(其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)
更精确地:
闭包包含环境部分,它是个只有两项的list,这两项分别是:
(1)环境。
(2)一个标识符或者标识符列表。
以及控制部分,他是个只有一项的list,唯一的项是函数对象本身。
闭包产生的原因?
闭包产生的本质就是,当前环境中存在指向父级作用域的引用,闭包中被引用的部分无法被垃圾回收。闭包的形成其实就是作用域链没有被释放,因此会造成内存泄露。
闭包有哪些表现形式?
返回一个函数。
function f1() { var a = 2 function f2() { console.log(a);//2 } return f2; } var x = f1(); x();
在函数外部调用。
var f3; function f1() { var a = 2 f3 = function() { console.log(a); } } f1(); f3();
作为函数参数传递。
var a = 1; function foo(){ var a = 2; function baz(){ console.log(a); } bar(baz); } function bar(fn){ // 这就是闭包 fn(); } // 输出2,而不是1 foo();
在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。(执行位置不在当前执行环境)
// 定时器 setTimeout(function timeHandler(){ console.log('111'); },100) // 事件监听 $('#app').click(function(){ console.log('DOM Listener'); })
IIFE(立即执行函数表达式)创建闭包, 保存了
全局作用域window
和当前函数的作用域
var a = 2; (function IIFE(){ // 输出2 console.log(a); })();
函数递归调用自身。
考点3:this
谈谈你对JS中 this 的理解。
this 是非常简单的东西。只需要理解它的执行规则就OK。
this 是运行时动态绑定,所以 this 的值与它所处的位置无关,而是与当前的执行环境(上下文)有关。
一些 this 隐式绑定的场景:
全局上下文默认this指向 window, 严格模式下指向 undefined。
直接调用。this相当于全局上下文。
let obj = { a: function() { console.log(this); } } let func = obj.a; func();
对象.方法
的情况,this指向这个对象。obj.method();
DOM事件绑定中,
onclick
和addEventerListener
中 this 默认指向绑定事件的元素。new + 构造函数,此时构造函数中的 this 指向实例对象。
箭头函数没有 this,因此也不能绑定。里面的 this 会指向当前最近的非箭头函数的 this,找不到就是window(严格模式是undefined)。
综上, this 的绑定是(按优先级):
- apply、call 将 this 绑定为第一个参数。new 方式调用函数,this 绑定返回的对象。
- this 绑定调用函数的上下文对象。
- this 默认绑定 global/window,严格模式是 undefined。
- 特殊,bind 只返回改变 this 指向的函数副本,不改变当前执行环境的 this。(返回的函数的执行上下文改变)
在
es6
箭头函数中:- 箭头函数不会创建 this,只会从父作用域继承 this。
- 指向最近的非箭头函数的 this。
- 如果没找到,this 指向 window(严格模式 undefined)。
this 传递的本质:
对
obj.method
访问的结果并非只是一个函数,而是一个Reference Type
的值。Reference Type
的值是一个三个值的组合(base, name, strict)
,其中:base
是对象。name
是属性名。strict
在use strict
模式下为 true。
当
()
被在Reference Type
上调用时,它们会接收到关于对象和对象的方法的完整信息,然后可以设置正确的this
。如何改变 this?
使用 call/apply/bind可以显示绑定:
let obj1 = { name: "a", getName() { console.log(this.name); }, }; let obj2 = { name: "b", }; obj1.getName() //a obj1.getName.call(obj2)//b obj1.getName.apply(obj2)//b obj1.getName.bind(obj2)()//b obj1.getName()//a
如何实现一个 call/apply 函数?
关键是如何实现向原函数传递数量不定的参数。使用 es6 的展开运算符可以轻松做到(如果不限定 es3)。
另外两个解决方案:
eval(string)
魔鬼new Function([arg1[, arg2[, ...argN]],] functionBody)
call:
//简易版,不考虑同名覆盖问题 Function.prototype.call = function (context, ...args) { context = context || window; context.fn = this; var result = context.fn(...args); delete context.fn return result; }
apply:
Function.prototype.apply = function (context, args) { context = context || window; context.fn = this; var result = context.fn(...args); delete context.fn return result; }
如何模拟实现一个 bind 的效果?
要实现 bind 效果,需要满足以下两点:
- 对于普通函数,绑定this指向。
- 对于构造函数,要保证原函数的原型对象上的属性不能丢失。
Function.prototype.myBind = function bind(thisArg) { if (typeof this !== "function") { throw new TypeError(this + " must be a function"); } // 存储调用bind的函数本身 var self = this; // 去除thisArg的其他参数 转成数组 var args = [].slice.call(arguments, 1); var bound = function () { // bind返回的函数 的参数转成数组 var boundArgs = [].slice.call(arguments); var finalArgs = args.concat(boundArgs); // new 调用时,其实this instanceof bound判断也不是很准确。es6 new.target就是解决这一问题的。 if (this instanceof bound) { // 这里是实现的 new 的第 1, 2, 4 步 // 1.创建一个全新的对象 // 2.并且执行[[Prototype]]链接 // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。 // self可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作。 if (self.prototype) { // ES5 提供的方案 Object.create() // bound.prototype = Object.create(self.prototype); // 但 既然是模拟ES5的bind,那浏览器也基本没有实现Object.create() // 所以采用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create function Empty() {} Empty.prototype = self.prototype; bound.prototype = new Empty(); } // 这里是实现的 new 的第 3 步 // 3.生成的新对象会绑定到函数调用的`this`。 var result = self.apply(this, finalArgs); // 这里是实现的 new 的第 5 步 // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`), // 那么`new`表达式中的函数调用会自动返回这个新的对象,否则返回函数的返回值。 var isObject = typeof result === "object" && result !== null; var isFunction = typeof result === "function"; if (isObject || isFunction) { return result; } return this; } else { // apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果 return self.apply(thisArg, finalArgs); } }; return bound;//返回的函数应具备普通函数和构造函数的功能 };
new 的模拟实现
new 做了什么?
创建一个全新的对象。
这个对象会被执行
[[Prototype]]
(也就是__proto__
)链接。生成的新对象绑定到函数调用的 this。
通过 new 创建的每个对象最终将被
[[Prototype]]
链接到函数的prototype
对象上。如果函数没有返回对象类型
Object
(Function
、Array
、Date
、RegExp
、Error
),那么 new 表达式的函数调用会自动返回这个新的对象。
function newOperator(ctor) { if (typeof ctor !== "function") { throw "newOperator function the first param must be a function"; } newOperator.target = ctor; var newObj = Object.create(ctor.prototype); var argsArr = [].slice.call(arguments, 1); var ctorReturnResult = ctor.apply(newObj, argsArr); var isObject = typeof ctorReturnResult === "object" && ctorReturnResult !== null; var isFunction = typeof ctorReturnResult === "function"; if (isObject || isFunction) { return ctorReturnResult; } return newObj; }
数组
考点1:类数组对象
函数的 arguments 为什么不是数组?
因为函数的 arguments 实质上是一个对象,其属性是 0,1,2……这样排列,最后还有 calle 和 length 属性。它类似于数组,却并不是数组。这样的对象叫做类数组对象。
常用的类数组还有:
- 用
getElementsByTagName/ClassName()
获得的HTMLCollection
- 用
querySelector
获得的nodeList
这样的话很多数组的方法就不能使用了。必要时需要将他们转化为数组。
- 用
类数组对象如何转化成数组?
实现方法很多:
Array.prototype.slice.call()
slice
返回原数组的浅拷贝。function sum(a, b) { let args = Array.prototype.slice.call(arguments); return args.reduce((sum, cur) => sum + cur); }
Array.prototype.splice.call()
splice
会返回删除的数组元素,可以利用这一点。function sum(a, b) { let args = Array.prototype.splice.call(arguments,0); return args.reduce((sum, cur) => sum + cur); }
Array.from()
从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
也可用来转换
Set、Map
function sum(a, b) { let args = Array.from(arguments); return args.reduce((sum, cur) => sum + cur); }
ES6 展开运算符
function sum(a, b) { let args = [...arguments]; return args.reduce((sum, cur) => sum + cur); }
concat + apply
利用
apply
方法会把第二个参数展开。function sum(a, b) { let args = Array.prototype.concat.apply([], arguments); console.log(args.reduce((sum, cur) => sum + cur)); }
原始方法,for 循环遍历。
考点2:数组遍历
forEach 中 return 有效果吗?如何中断 forEach 循环?
在 forEach 中 return 函数不会返回。
中断方法:
使用
try...catch
在catch
中捕获异常并抛出新异常。官方推荐方法(替换方法):用
every
和some
替代 forEach 函数。every
在碰到return false
的时候,中止循环。some
在碰到return true
的时候,中止循环。every
和some
都是对数组的每个元素用一个指定的函数检测。区别在于检测的结果。every
全部通过返回true
,some
至少一个通过返回true
判断数组中是否包含某个值
Array.prototype.indexOf()
arr.indexOf(searchElement[, fromIndex])
返回在数组中可以找到给定元素的第一个索引。不存在返回
-1
var arr=[1,2,3,4]; var index=arr.indexOf(3); console.log(index);//2
Array.prototype.includes()
arr.includes(valueToFind[, fromIndex])
根据数组中是否存在指定元素返回 true 和 false
var arr=[1,2,3,4]; if(arr.includes(3)) console.log("存在"); else console.log("不存在");
Array.prototype.find()
arr.find(callback[, thisArg])
返回满足测试函数的第一个元素的值
var arr=[1,2,3,4]; var result = arr.find(item =>{ return item > 3 }); console.log(result);
Array.prototype.findIndex()
返回满足测试函数的第一个元素的索引,不存在返回
-1
var arr=[1,2,3,4]; var result = arr.findIndex(item =>{ return item > 3 }); console.log(result);
考点3:数组扁平化
实现数组 flatten 方法
多维数组转化为一维数组。
es6 的
flat
方法按照指定的深度递归遍历数组,将所有元素和子数组的元素合并为一个新数组返回。
ary = ary.flat(Infinity);
replace + split + map
replace
将[]
替换为空字符串,split
分割,map
转换为数字。let str = JSON.stringify(ary) ary = str.replace(/(\[|\])/g, '').split(',').map(item=>+item)
replace + JSON.parse
replace
替换字符串,JSON.parse
转化为对象。let str = JSON.stringify(ary); str = str.replace(/(\[|\])/g, ''); str = '[' + str + ']'; ary = JSON.parse(str);
普通递归
function flatten(arr) { let res = []; for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { res = res.concat(flatten(arr[i])); } else { res.push(arr[i]); } return res; } }
reduce
迭代function flatten(arr) { return arr.reduce( (pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur) : cur), [] ); }
function flatten(arr) { return Array.isArray(arr) ? arr.reduce((pre, cur) => [...pre, ...flatten(cur)], []) : [arr]; }
扩展运算符
利用展开语法结合
concat
特性function flatten(arr) { while (arr.some(Array.isArray)) { arr = [].concat(...arr); } return arr; }
考点4:数组去重
实现数组去重,方法越多越好
Set 去重
有个小问题,不能完美处理稀疏数组
[...new Set(arr)]
Map 去重
function arrayNonRepeatfy(arr) { let map = new Map(); let array = new Array(); for (let i = 0; i < arr.length; i++) { if (!map.has(arr[i])) { map.set(arr[i]); if (i in arr) array.push(arr[i]); //处理稀疏数组情况 } } return array; }
两层循环遍历 + splice 去重
缺点是会改变原数组
function arrayNonRepeatfy(arr) { for (let i = 0; i < arr.length; i++) { for (j = i + 1; j < arr.length; j++) { if (arr[i] == arr[j]) { arr.splice(j, 1); j--; } } } return arr; }
一层循环遍历 + indexOf 去重
function arrayNonRepeatfy(arr) { let array = new Array() for (let i = 0; i < arr.length; i++) { if(arr.indexOf(arr[i]) == i){ array.push(arr[i]) } } return array; }
sort 排序两两比较去重
function arrayNonRepeatfy(arr) { let array = new Array() arr = arr.sort((a,b)=>a-b) for (let i = 0; i < arr.length; i++) { if(arr[i] !== arr[i+1]){ array.push(arr[i]) } } return array; }
一层循环遍历 + includes 去重
function arrayNonRepeatfy(arr) { let array = new Array(); for (let i = 0; i < arr.length; i++) { if (i in arr) { if (!array.includes(arr[i])) { array.push(arr[i]); } } } return array; }
filter + indexOf 去重
function arrayNonRepeatfy(arr) { return arr.filter((prev, index, arr) => arr.indexOf(prev) == index); }
reduce + includes 去重
function arrayNonRepeatfy(arr) { return arr.reduce( (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]), [] ); }
考点5:高阶函数
什么是高阶函数?
一个函数可以接收一个或多个函数作为参数或者返回值为一个函数。
可归纳为:
- 输入上接收一个或多个函数作为参数。
- 输出上返回一个函数。
数组中的高阶函数有哪些?
Array.prototype.map()
创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值。
语法:
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
参数:
callback
:生成新数组元素的函数,使用三个参数:currentValue
:callback
数组中正在处理的当前元素。index
可选:callback
数组中正在处理的当前元素的索引。array
可选:map
方法调用的数组。
thisArg
可选:执行callback
函数时值被用作this
。Array.prototype.reduce()
对数组中的每一个元素执行提供的
reducer
函数,将其结果汇总为单个返回。语法:
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
参数:
callback
:执行数组中每个值 (如果没有提供initialValue则第一个值除外
)的函数,包含四个参数:accumulator
:累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue
(见于下方)。currentValue
:数组中正在处理的元素。index
可选:数组中正在处理的当前元素的索引。 如果提供了initialValue
,则起始索引号为0,否则从索引1起始。array
可选:调用reduce()
的数组
initialValue
可选:作为第一次调用callback
函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。Array.prototype.filter()
创建一个新数组,包含通过提供的函数测试的所有元素。
语法:
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
参数:
callback
:用来测试数组的每个元素的函数。返回true
表示该元素通过测试,保留该元素,false
则不保留。它接受以下三个参数:element
:数组中当前正在处理的元素。index
可选:正在处理的元素在数组中的索引。array
可选:调用了filter
的数组本身。
thisArg
可选:执行callback
时,用于this
的值。Array.prototype.sort()
对数组元素进行 排序,并返回数组。
语法:
arr.sort([compareFunction])
参数:
compareFunction
可选:用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的 Unicode 位点进行排序。firstEl
:第一个用于比较的元素。secondEl
:第二个用于比较的元素。
考点6:数组常用 API 实现
能不能实现数组 forEach 方法?
需要注意的就是使用 in 来进行原型链查找。同时,如果没有找到就不处理,能有效处理稀疏数组的情况。
Array.prototype.myforEach = function (callbackFn, thisArg) { //处理数组类型异常 if (this == null) { throw new TypeError("Cannot read property 'forEach' of null or undefined"); } //处理回调类型异常 if (Object.prototype.toString.call(callbackFn) != "[object Function]") { throw new TypeError(callbackFn + " is not a function"); } //草案中提到要先转换为对象 let O = Object(this); //保证len为数字且为整数 let len = O.length >>> 0; for (let i = 0; i < len; i++) { if (i in O) { callbackFn.call(thisArg, O[i], i, O); } } };
能不能实现数组 map 方法?
与 forEach 类似,注意返回的数组与原数组等长的细节。
Array.prototype.map = function (callbackFn, thisArg) { //处理数组类型异常 if (this == null) { throw new TypeError("Cannot read property 'map' of null or undefined"); } //处理回调类型异常 if (Object.prototype.toString.call(callbackFn) != "[object Function]") { throw new TypeError(callbackFn + " is not a function"); } //草案中提到要先转换为对象 let O = new Object(this); let T = thisArg; //保证len为数字且为整数 let len = this.length >>> 0; let A = new Array(len); for (let k = 0; k < len; k++) { if (k in O) { let kValue = O[k]; //依次传入this,当前项,当前索引,整个数组 let mapValue = callbackFn.call(T, kValue, k, O); A[k] = mapValue; } } return A; };
能不能实现数组 reduce 方法?
注意传不传初始值时的情况要分开处理。
Array.prototype.reduce = function (callbackFn, initialValue) { //处理数组类型异常 if (this == null) { throw new TypeError("Cannot read property 'reduce' of null or undefined"); } //处理回调类型异常 if (Object.prototype.toString.call(callbackFn) != "[object Function]") { throw new TypeError(callbackFn + " is not a function"); } //草案中提到要先转换为对象 let O = new Object(this); let len = this.length; let k = 0; let accumulator = initialValue; //不传初始值,默认找数组第一项 if (accumulator === undefined) { for (; k < len; k++) { if (k in O) { accumulator = O[k]; k++; break; } } } //表示数组全为空 if (k === len && accumulator === undefined) { throw new Error("Each element of the array is empty"); } for (; k < len; k++) { if (k in O) { let kValue = O[k]; //依次传入 累加器,当前项,当前索引,整个数组 accumulator = callbackFn.call(undefined, accumulator, kValue, k, O); } } return accumulator; };
能不能实现数组 push、pop 方法 ?
push 方法:
注意处理数组长度细节即可。
Array.prototype.push = function (...items) { let O = Object(this); let len = O.length >>> 0; let argCount = items.length >>> 0; //处理数组长度异常 if (len + argCount > 2 ** 53 - 1) { throw new TypeError( "The number of array is over the max value restricted!" ); } for (let i = 0; i < argCount; i++) { O[len + i] = items[i]; } let newLength = len + argCount; O.length = newLength; return newLength; };
pop 方法:
注意在删除前存储值。
Array.prototype.pop = function () { let O = Object(this); let len = O.length >>> 0; //处理数组长度异常 if (len === 0) { O.length = 0; return undefined; } len--; let value = O[len]; delete O[len]; O.length = len; return value; };
能不能实现数组filter方法 ?
与 map 类似,注意返回值是通过测试项。
Array.prototype.filter = function (callbackFn, thisArg) { // 处理数组类型异常 if (this === null || this === undefined) { throw new TypeError("Cannot read property 'filter' of null or undefined"); } // 处理回调类型异常 if (Object.prototype.toString.call(callbackFn) != "[object Function]") { throw new TypeError(callbackFn + " is not a function"); } let O = Object(this); let len = O.length >>> 0; let resLen = 0; let res = []; for (let i = 0; i < len; i++) { if (i in O) { let element = O[i]; //依次传入 当前项,当前索引,整个数组 if (callbackFn.call(thisArg, O[i], i, O)) { res[resLen++] = element; } } } return res; };
能不能实现数组 slice 方法?
难点在于处理参数边界情况(非 number 类型和小数、负数、越界情况)。
Array.prototype.slice = function (begin, end) { // 处理数组类型异常 if (this == null) { throw new TypeError("Cannot read property 'slice' of null or undefined"); } let array = Object(this); let len = array.length >>> 0; //参数边界处理 //省略start,默认从 0 开始 let start = isNaN(Number(begin)) ? 0 : Number(begin); if (start > 0) start = start > len ? len : Math.floor(start); if (start < 0) //start + len > 0, start 表示倒数第几项,否则为 0 start = Math.ceil(start) + len < 0 ? 0 : Math.ceil(start) + len; //省略 end,提取到数组末尾 let upTo = isNaN(Number(end)) ? len : Number(end); if (upTo > 0) upTo = upTo > len ? len : Math.floor(upTo); if (upTo < 0) upTo = Math.ceil(upTo) + len < 0 ? 0 : Math.ceil(upTo) + len; let size = Math.max(0, upTo - start); let res = new Array(size); // 不使用数组方法,因为 slice 可转换类数组对象 for (let i = start; i < start + size; i++) { if (i in array) res[i - start] = array[i]; } return res; };
能不能实现数组 splice 方法?
注意重排序在不同情况时的遍历顺序。
Array.prototype.splice = function (start, deleteCount, ...items) { let array = Object(this); let len = array.length >>> 0; //参数边界处理 //startIndex默认从 0 开始 start = isNaN(Number(start)) ? 0 : Number(start); if (start > 0) start = start > len ? len : Math.floor(start); if (start < 0) //start + len > 0, start 表示倒数第几项,否则为 0 start = Math.ceil(start) + len < 0 ? 0 : Math.ceil(start) + len; //省略 deleteCount,start之后数组的所有元素都会被删除。 deleteCount = isNaN(Number(deleteCount)) ? len - start : Number(deleteCount); if (deleteCount > 0) deleteCount = deleteCount > len - start ? len - start : Math.floor(deleteCount); if (deleteCount < 0) deleteCount = 0; // 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象 if (Object.isSealed(array) && deleteCount !== items.length) { throw new TypeError("the object is a sealed object!"); } else if (Object.isFrozen(array) && (deleteCount > 0 || items.length > 0)) { throw new TypeError("the object is a frozen object!"); } let deleteArr = new Array(deleteCount); // 拷贝删除元素 for (let i = 0; i < deleteCount; i++) { let index = start + i; if (index in array) { let current = array[index]; deleteArr[i] = current; } } //重排序 const moveElements = (start, deleteCount, ...items) => { if (deleteCount === items.length) { return; } // 删除大于插入,整体前移 if (deleteCount > items.length) { for (let i = start + deleteCount; i < array.length; i++) { // 从最后删除的元素下标开始 let from = i; let to = i - deleteCount + items.length; if (from in array) { array[to] = array[from]; } else { delete array[to]; } } for ( let i = array.length - 1; i >= array.length + items.length - deleteCount; i-- ) { delete array[i]; } } // 删除小于插入,整体后移 if (deleteCount < items.length) { for (let i = array.length - 1; i > start + deleteCount; i--) { let from = i; let to = from - deleteCount + items.length; if (from in array) { array[to] = array[from]; } else { delete array[to]; } } } }; moveElements(start, deleteCount, ...items); // 插入新元素 for (let i = 0; i < items.length; i++) { array[start + i] = items[i]; } array.length = array.length - deleteCount + items.length; return deleteArr; };
节流防抖
考点1:节流
能不能实现一个节流器?
传统写法:
function throttled(fn, delay) { let flag = true; return function (...args) { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, args); flag = true; }, delay); }; }
简洁写法:
function throttled(fn, delay) { let timer; return function (...args) { if (!timer) { timer = setTimeout(() => { fn.apply(this, args) timer = null }, delay); } } }
加强写法:
function throttled(fn, delay) { let timer; let startTime = Date.now(); return function (...args) { let curTime = Date.now(); let remaining = delay - (curTime - startTime); let context = this; clearTimeout(timer); if (remaining <= 0) { fn.apply(context, args); startTime = Date.now(); } else { timer = setTimeout(() => fn.apply(context, args), remaining); } }; }
考点2:防抖
能不能实现一下防抖器?
传统防抖:
function debounce(fn, interval) { let timer; return function (...args) { if (timer) clearTimeout(timer); let context = this; timer = setTimeout(() => { fn.apply(context, args); }, interval); }; }
首次立刻执行的防抖:
function debounce(fn, delay, immediate) { let timeout; return function (...args) { if (timeout) clearTimeout(timeout); if (immediate) { let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发 timeout = setTimeout(() => { timeout = null; }, delay); if (callNow) { fn.apply(this, args); } } else { timeout = setTimeout(() => { fn.apply(this, args); }, delay); } }; }
考点3:合并
能不能实现同时防抖和节流?
function throttle(fn, delay) { let last = 0, timer = null; return function (...args) { let context = this; let now = new Date(); if(now - last < delay){ clearTimeout(timer); setTimeout(function() { last = now; fn.apply(context, args); }, delay); } else { // 这个时候表示时间到了,必须给响应 last = now; fn.apply(context, args); } } }
后记
JS进阶内容见 esnext 篇。
- Post link: https://blog.sticla.top/2021/08/12/front-end-interview-review-js/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.
GitHub Issues