阅读 791

前端面试复习:js 篇,30岁的我找工作好难 | 掘金技术征文

前言

开头 banner 是我这几晚写文章,女儿在看动画片,孩子她妈拍摄的(是不是还算和谐!)。

3天前写了 《前端面试复习:网络篇 | 30岁的我找工作好难》,感觉还不错,对我准备面试起到总结性复习作用。本着不能食言的好品质,这次赶紧续更 Javascript 相关知识点。

不过 js 的范畴太大了,根据二八定律,下面的内容可能只占面试中 js 方向 20% 的内容(甚至还更少)。但我希望有幸阅读到此文的掘友,多多少少能和我有一样有收获(若还不错,希望能点个赞,鼓励我下^.^)。

最后:如果哪些阐述有问题,望好心人评论告知,我将及时修改。

0.1+0.2 ≠0.3

这是个 js 里老生常谈的精度问题,详细可在【参考文章】了解更多。

认识双精度浮点数

js 采用 IEEE 754 标准,以 64 位双精度表示浮点数值。

具体 64 位的含义,如下:

  • sign bit(S 符号):符号位,表示正负号(0 为负数,1 为正数)
  • exponent(E 指数):表示次方数,在(二进制的)科学计数法中定义 2 的多少次幂
  • mantissa(M 尾数):表示精确度(小数部分,规范中会省略个位数上的 1 )

最后数值会以如下公式来体现:

指数偏移值

注意上面的数值公式:指数值通过 exponent-0x3ff 表示。其中 0x3ff(16) 为 1023(10),为什么要减去这个值呢?

因为指数有 11 位,那么示意正负值就有 -1024~1023 共 2048 个数字,但这里 11 位是不包含符号位的,所以根据负数最高位 1 将这些数字拆分为两个区间: (0,1023] 区域将表示正数,[1024,2048) 表示负数,两端最值有特殊作用。

看似没有问题,那来个 灵魂拷问指数值分为 1000 和 2000,试问哪个大?

按照上面规则,我们知道 2000 属于负数,常识告诉我们 正数大于负数,所以 1000 > 2000?是不是很奇怪?

为了消除这个矛盾,引入了 指数偏移值。 将指数值增加个 1024 偏移量,那整个指数取值将为 0~2048 这些结果值,这样就不存在前面负数大于正数的问题。由于最值有特殊作用,则偏移量为 1023。最终将变为 1000+1023 和 2000+1023 之间的指数对比,则:2023 < 3023

回到精度问题

0.1 的二进制为 0.0001100110011... (复制来的,我算不来,哈哈);

科学计数法(2进制)表示为:1.100110011... x 2^(-4) ;

根据数值公式,表示为:S=1,E=-4+1023,M=10011 0011 0011 ...(共 52 位)

同理:0.1 的二进制为 0.001100110011... ;

科学计数法(2进制)表示为:1.100110011... x 2^(-3) ;

根据数值公式,表示为:S=1,E=-3+1023,M=10011 0011 0011 ...(共 52 位)

说了那么多,其实只是阐述 64 位双精度值的存储方式。说白了,就是精度控制在 52 位(不管是不是无限循环)

最后相加后为:0.01001100110011001100110011001100110011001100110011001110 ,转化为 10 进制就为:0.30000000000000004

原型

相关概念

当一个构造函数 User 方法声明后,将自带 prototype 属性,其指向该构造函数的 原型对象(User Prototype)。我们可以往原型对象上设置属性 or 方法,就像这样:

function User() {}
User.prototype.name = 'Eminoda';
User.prototype.age = 29;
复制代码

注意:因为 User.prototype 属性指向 User Prototype 原型对象,简单认为 User.prototype 就是原型对象的引用,但还是要明白 User.prototype 和 User Prototype 是两回事。

原型对象 内含 constructor 属性,并指回构造函数 User:

接着讲说下构造函数实例出来的对象 user,实例化后的对象将含有一个 _proto_ 属性(或者为 [[Prototype]]),它是个非显性的属性。 我们可以显示的来调用它:

var user = new User();
user.__proto__ //
复制代码

并发现它的引用结果是指向 User Prototype 原型对象:

到此,原型相关的基础概念简单普及了下。加下来讲讲 什么是原型链

我们已经知道原型对象是指向构造函数 User 的(即 User.prototype 的引用指向 User),如果我们 将此引用替换为某构造函数的实例,会有什么结果?

function Parent(){}
User.prototype = new Parent();
复制代码

按前面 _proto_ 的规则(实例属性将包含一个指向原型对象的 _proto_ 引用),那么这时的 User.prototype 也将包含这个 _proto_ 引用,并且指向 Parent Prototype 原型对象

image.png

就这样他们一级一级引用上去,直至顶级的 Object 原型对象。画张图,将是这个样子:

image.png

Object.getPrototyeOf()

返回指定 object 的原型对象(内部[[Prototype]]属性的值)。

Object.getPrototypeOf(object)

就是 Object.getPrototype 方法,获取传入对象 object 对应的原型对象引用:

Object.getPrototypeOf(parent) === Parent.prototype //true

Object.getPrototypeOf(user) === User.prototype //true
复制代码

这里提一嘴,User.prototype 上面已经改变了引用,虽然上面只反应了引用是否相等,但其实结果是 Parent 的实例:

image.png

isPrototypeOf 和 instanceof

这两个太混淆了

prototypeObj.isPrototypeOf(object)

用于测试一个对象 object 是否存在于另一个对象 prototypeObj 的原型链上。

我们先看下 Parent 构造函数实例化后的 parent 对象的效果:

var parent = new Parent();

Parent.prototype.isPrototypeOf(parent) //true
Object.prototype.isPrototypeOf(parent) //true

User.prototype.isPrototypeOf(parent) //false
User.prototype.isPrototypeOf(user) //true
复制代码

来分析下:

  • 首先实例 parent 对象的属性 _proto_ 指向 Parent 的原型对象,Parent.prototype 就是该原型对象的引用,则 Parent.prototype.isPrototypeOf(parent) 成立
  • 再次,Parent 的原型对象中的 _proto_ 指向 Object 的原型对象,同样 Object.prototype 就是该原型对象的引用,这都是依照原型链找到的,则 Object.prototype.isPrototypeOf(parent) 成立
  • 虽然 User.prototype 在上面实现继承时改变了引用,指向 Parent 创建的实例,但这不是 parent 上的原型链(通俗讲,parent 还高一级),所以之后判断 user 对象时,则会成立

object instanceof constructor

用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链

user instanceof User //true
user instanceof Parent //true
user instanceof Object //true

parent instanceof User //false
复制代码

似乎和上面 isPrototypeOf 差不多作用。但有那么点不一样:

首先,Foo Prototype 的原型对象会有 constructor 属性,其指向 Foo 构造函数,instanceof 右侧的对比内容其实是这个 Foo 构造函数的 prototype 属性所指的 Foo Prototype 原型对象。和左侧 object 对象的原型链做比较。

所以有那么一句话:

object instanceof AFunction 比的是右侧 AFunction.prototype,而非 AFunction 本身

而 isPrototypeOf 是把 AFunction.prototype 直接摆上台面。

Parent.prototype = {}
user instanceof Parent //false

Parent.prototype.isPrototypeOf(parent) //false
复制代码

hasOwnProperty

判断构造函数上是否有定义某属性,而 不包扩原型链上的

var user = new User();
User.prototype.name = 'Eminoda';
User.prototype.age = 29;

user.name //"Eminoda"
user.hasOwnProperty('name') //false
复制代码

in(for-in)

比 hasOwnProperty 范围大,包括原型上定义的属性。

'name' in user //true
复制代码

Object.keys

上面两个方法是用来判断用的,这个 Object.keys 则用来枚举对象的属性,当然 不包含原型属性。但可以指定原型对象,来枚举。

Object.keys(User) //[]
Object.keys(User.prototype) //["name", "age"]
复制代码

继承方式

js 因为其语言的特性,所以对象的继承只能依靠原型来完成。

原型链继承

function Parent(){
  this.name = 'parent';
}
Parent.prototype.pName = 'parent_prototype_name';
Parent.prototype.skill = ['vue']

function Child(){
  this.name = 'child'
}
Child.prototype = new Parent(); //修改 Child 原型对象的引用,指向 Parent 实例,
Child.prototype.cName = 'child_prototype_name'

var child = new Child();

console.log(child.name); //child
console.log(child.cName); //child_prototype_name
console.log(child.pName); //parent_prototype_name

child.skill.push('react');

var child2 = new Child();
console.log(child2.skill); //["vue", "react"]
复制代码

缺点:

  • Child 原型对象指向 Parent 实例,后续每个 Child 实例都共享 Parent 原型属性,如果原型上涉及数组等对象属性像有数据污染的问题
  • 无法向 Parent 构造函数传参

借用构造函数继承

function Parent(name){
  this.name = name;
  this.name2 = 'name2'
}
Parent.prototype.pName = 'parent_prototype_name';
Parent.prototype.skill = ['vue']

function Child(name){
  Parent.call(this,name)
}
Child.prototype.cName = 'child_prototype_name'

var child = new Child('c1');

console.log(child.name); //c1
console.log(child.cName); //child_prototype_name

console.log(child.name2); //name2
console.log(child.pName); //undefined
复制代码

能看到可以给父 Parent 构造函数传入参数,相当于 super 功能; 缺点:

  • 虽然借用 Parent 构造函数,可以获取父类上的 name2 属性或者方法,但每个 Child 实例都自带一份 Parent 副本,谈不上方法复用(不优雅)
  • 没有之前原型链的特性,无法享受共享的红利

组合式继承

function Parent(name){
  this.name = name;
  this.name2 = 'name2';
  this.skill = ['vue'];
}
Parent.prototype.pName = 'parent_prototype_name';

function Child(name){
  Parent.call(this,name) //使用 Parent 构造函数第一次
}
Child.prototype = new Parent(); //使用 Parent 构造函数第二次
Child.prototype.cName = 'child_prototype_name'

var child = new Child('c1');

console.log(child.name); //c1
console.log(child.cName); //child_prototype_name

console.log(child.name2); //name2
console.log(child.pName); //parent_prototype_name
复制代码

结合上两者的问题,解决了构造传参问题和属性方法共享。

对于可以共享的属性 or 方法,我们放到 Parent 的原型对象中定义;同时,Child 内部依旧借用 Parent 来获取父类的属性 or 方法。

缺点: 注意到此继承过程中对 Parent 构造函数调用了两次,一次是借用时,第二次是更换 Child 原型对象的引用时创建。

寄生组合式

代码基本和上述相同,只是把 Child.prototype = new Parent(); 做个特殊处理:

//...
function inherits(Child, Parent) {
	// 没有像组合继承那样 new Parent,而是通过原型对象来创建一个新的对象
	var prototype = Object.create(Parent.prototype);
	// 重新设定引用,Child而非Parent或其他
	prototype.contructor = Child;
	Child.prototype = prototype;
}
inherits(Child, Parent); //Child.prototype = new Parent();
//...
复制代码

利用 Object.create 来创建一个针对 Parent.prototype 原型对象的新对象实例,避免 Parent 构造函数的使用。

this 的一些理解

错误理解

指向自身

这个我们遇到的比较多,通常在不注意的情况下,this 指向了浏览器的 window

function say() {
	console.log(this.foo);
	this.foo++;
	console.log(this.foo);
}
var foo = 1;
say.foo = 0;
for (var i = 0; i < 5; i++) {
	say();
}
console.log(say.foo); //0
复制代码

作用域

试问 bar 方法内 this.a 输出什么?

function bar() {
	console.log("bar called");
	console.log(this.a); // undefined
}

function foo() {
	var a = 1;
	this.bar();
}

foo();
复制代码

看似调用栈 foo -> bar -> foo.a,但其实 this 是 js 引擎内部运行时产生的,并不是简单拍脑袋以为词法作用域所能串联起来的。同时更不可能在 bar 中试图通过 this 来获取 foo 的 a 属性。

四种绑定方式

这就涉及如下几种 this 的绑定方式:

默认绑定

在没有特殊规则 or 指定的情况下,this 默认指向全局环境,比如 window 。

隐藏绑定

确认是否有上下文(执行环境)的影响。

function foo() {
	console.log(this.a);
}
var obj = {
	a: 1,
	foo: foo
};
obj.foo(); //1
复制代码

上面这个例子中,foo 是一个函数声明并且在声明时 提升到全局,但在被 obj 调用时,函数 foo 的 引用 给予了 obj.foo,导致最后 this 的上下文环境变更到了 obj 中。

再试一例:

function foo() {
	console.log(this.a);
}
var obj = {
	a: 1,
	foo: foo
};

var foo2 = obj.foo; // 引用又被拎到外面
var a = 2;

obj.foo(); //1
foo2(); //2
复制代码

显示绑定

使用 call, apply 之类的方法,这我们比较熟悉。

new 绑定

function User() {
	this.name = "aaaa";
}
var name = "bbbb";
var user = new User();
console.log(user.name); // aaaa
复制代码

类型的判断

数据类型分类

  • 复杂类型(引用类型)
    • object
    • array
    • function
  • 简单类型(值类型)
    • null
    • undefined
    • boolean
    • number
    • string
    • symbol (es6)
    • bigint (es10)

判断类型

typeof

对于简单类型,基本 typeof xxx == "简单类型",唯独 null 不一样

typeof null 	        //"object"
typeof undefined 	//"undefined"
typeof 123 		//"number"
typeof 'eminoda' 	//"string"
typeof Symbol(1) 	//"symbol"
typeof BigInt(1) 	//"bigint"
复制代码


另外在复杂类型中,也有那么些不一样:

typeof {} 	//"object"
typeof []	//"object"
typeof /[0-9]/	    //"object"
typeof function(){} //"function"
复制代码

另外,这里提个小 tips,对于未声明的变量,使用 typeof 还有一层 安全机制检查 作用:

console.log(foo); //error foo is not defined

if(typeof foo=='undefined'){
  console.log('come in');
}
复制代码

toString

上面看到复杂类型的数据通过 typeof 无法很好的区分,这是因为这些引用类型一般都是由 js 内置对象产生(String、Number、Boolean、Object、Array、Function、Date、RegExp、Error、BigInt

所以精确区分,可以通过如下手段(摘自:vue 代码)

const _toString = Object.prototype.toString

export function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

export function isRegExp (v: any): boolean {
  return _toString.call(v) === '[object RegExp]'
}

复制代码

constructor

当然还有如下方式,以 obj.constructor == Ctor 来判断:

/[0-9]/.constructor == RegExp
[1,2,3].constructor == Array
复制代码

闭包

贴下阮一峰老师的两个 Demo,其他就不多说了:

var name = "The Window";

var object = {
  name : "My Object",

  getNameFunc : function(){
    return function(){
      return this.name;
    };
  }
};

alert(object.getNameFunc()());
复制代码
var name = "The Window";

var object = {
  name : "My Object",
  getNameFunc : function(){
    var that = this;
    return function(){
      return that.name;
    };
  }
};

alert(object.getNameFunc()());
复制代码

数组一些方法说明

我到现在拼不大来 splice 或者 slice 之类的单词,tab 按管了。

splice

对数组做删除,添加元素操作。会影响原数组

var originArr = [2,3,4,1];
// 删除操作
var r1 = originArr.splice(2,1);//[4] 从角标2开始删除1个元素
originArr; //[2, 3, 1]

// 添加操作
var originArr = [2,3,4,1];
var r2 = originArr.splice(1,0,7,8); //[] 从角标1开始添加两个元素 7,8
originArr; //[2, 7, 8, 3, 4, 1]
复制代码


一个常用的操作:从数组中删除某个元素:

var originArr = ['a','b','c'];
// vue
function remove(arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}
console.log(remove(originArr, "a")); //[ 'a' ]
console.log(originArr); //["b", "c"]
复制代码

slice

根据设置的开始和结束索引,返回一个新数组对象。原数组不变

var originArr = [2, 3, 4, 9, 6, 7];

var r1 = originArr.slice(2); //[ 4, 9, 6, 7 ] 从索引2开始,复制数组(浅复制)
originArr; //[ 2, 3, 4, 9, 6, 7 ]

// 结束索引为负数,从右侧开始倒序获取
var r2 = originArr.slice(1, -3); //[3, 4]
originArr; //[ 2, 3, 4, 9, 6, 7 ]
复制代码

includes & indexOf

这两个要一同讲,虽然我平时之用 indexOf ,但在如下情况你就能分清这两者的区别:

var arr = [1,2,NaN,undefined]
arr.includes(NaN) //true
arr.indexOf(NaN) //-1

arr.includes(undefined) //true
arr.indexOf(undefined) //3

var arr = new Array(10) //(10) [empty × 10]
arr.includes(undefined) //true
arr.indexOf(undefined) //-1
复制代码

如果要得到元素在数组中的具体索引位置用 indexOf,但不能用在一个“空”数组中,这个值需要排除 NaN 情况;如果只是判断元素存不存在,includes 更适合。

filter & every & some

filter 具备过滤的作用,最后返回符合条件的结果集。 而 every 和 some 虽然也有过滤的意思,但只做是否符合条件的判断。

var originArr = [1,2,3,4,5,6];
let r1 = originArr.filter((item)=>item>3) //[4, 5, 6]
let r2 = originArr.every((item)=>item>3) //false
let r3 = originArr.some((item)=>item>3) //true
复制代码

map

根据处理逻辑,返回一个新数组:

var originArr = [1,2,3,4,5,6];
originArr.map((item)=>{
 if(item>3){
   return item*2;
 }else{
   return false
 }
}); //[false, false, false, 8, 10, 12]
复制代码

flat & flatMap

flat 用来对数组内部有嵌套数组做展开操作(默认展开1级深度):

var arr1 = [1, 2, [3, 4]];
arr1.flat(); // 默认1级
// [1, 2, 3, 4]

var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

var arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]
复制代码

MDN 上有个 pollfill 的例子,用的非常巧妙:

var arr1 = [1, 2, [3, 4]];
arr1.flat();

// reduce 和 concat 的运用
arr1.reduce((acc, val) => acc.concat(val), []);// [1, 2, 3, 4]

// 更多深度的数组嵌套
function flattenDeep(arr1) {
   return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
}
flattenDeep(arr1);// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
复制代码

flatMap 和 map 类似,但前者额外会对每次映射的结果做展开,合并到新数组中:

var arr1 = [1, 2, 3, 4];

arr1.map(x => [x * 2]); 
// [[2], [4], [6], [8]]

arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]

// only one level is flattened
arr1.flatMap(x => [[x * 2]]);
// [[2], [4], [6], [8]]
复制代码


使用场景举例,对一些特殊的数组数据做简单处理,使其马上返回一个新数组:

let arr1 = ["it's Sunny in", "", "California"];

arr1.map(x => x.split(" "));
// [["it's","Sunny","in"],[""],["California"]]

arr1.flatMap(x => x.split(" "));
// ["it's","Sunny","in", "", "California"]
复制代码

pollfill:

var arr = [1, 2, 3, 4];

arr.flatMap(x => [x, x * 2]);
// is equivalent to
arr.reduce((acc, x) => acc.concat([x, x * 2]), []);
// [1, 2, 2, 4, 3, 6, 4, 8]
复制代码

reduce

上面的 flat 中,已经看到了相关 reduce 的用法示意。这里再补充一个累加方式:

const array1 = [1, 2, 3, 4];
const reducer = function(accumulator, currentValue, index) {
  console.log(`第${index}调用---`);
  console.log(`accumulator:${accumulator},currentValue:${currentValue}`);
  return accumulator + currentValue;
};
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer)); //10
// 第1调用---
// accumulator:1,currentValue:2
// 第2调用---
// accumulator:3,currentValue:3
// 第3调用---
// accumulator:6,currentValue:4
复制代码

以及数组去重示例:

let arr = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
let result = arr.sort().reduce((init, current) => {
    if (init.length === 0 || init[init.length - 1] !== current) {
        init.push(current);
    }
    return init;
}, []);
console.log(result); //[1,2,3,4,5]
复制代码

另外,在下面的 手撕章节 中会有个数组转树形结构的例子(以前面试问的,反正当时很囧,都不知道写了啥)

正则

匹配规则

涉及 api

test

test 不说了,用来根据正则判断数据是否匹配

var re = /[0-9]/g;
var result = re.test('a123'); //true
复制代码

exec & match

前者是正则的方法,后者是 String 的方法,但是在某些用途上是有不同的:

var data = 'web2.0 .net2.0';
var pattern = /(\w+)(\d)\.(\d)/g;// 注意,全局模式

//["web2.0", "web", "2", "0", index: 0, input: "web2.0 .net2.0", groups: undefined]
pattern.exec(data);
//["net2.0", "net", "2", "0", index: 8, input: "web2.0 .net2.0", groups: undefined]
pattern.exec(data);

["web2.0", "net2.0"]
data.match(pattern);
复制代码

如果取消模式,他们的结果就一致了:

var data = 'web2.0 .net2.0';
var pattern = /(\w+)(\d)\.(\d)/;

//["web2.0", "web", "2", "0", index: 0, input: "web2.0 .net2.0", groups: undefined]
pattern.exec(data);
//["web2.0", "web", "2", "0", index: 0, input: "web2.0 .net2.0", groups: undefined]
data.match(pattern);
复制代码

分组捕获

捕获括号 capturing parentheses

正则表达式内部使用 (...) 括号,如果括号内有匹配内容将在结果中显示出来:

/((\d{4})-((\d{2})-(\d{2})))/.exec('2018-12-17');
// ["2018-12-17", "2018-12-17", "2018", "12-17", "12", "17", index: 0, input: "2018-12-17", groups: undefined]

/(\d{4})-((\d{2})-(\d{2}))/.exec('2018-12-17');
// ["2018-12-17", "2018", "12-17", "12", "17", index: 0, input: "2018-12-17", groups: undefined]
复制代码

非捕获括号 non-capturing parentheses

内部使用 (?:...),和上面相反,像省略括号匹配的内容:

/(?:\d{4})-(?:\d{2})-(?:\d{2})/.exec('2018-12-17');
// ["2018-12-17"]
复制代码

肯定,否定查找

正向查找 lookahead

括号内匹配中的不会出现在结果中,如果不匹配正则将无匹配结果:

/aaa(?=\d+)/.exec('aaa123'); // ["aaa", index: 0, input: "aaa123", groups: undefined]
/aaa(?=\d+)/.exec('aaabbb'); // null
/aaa(?=\d+)/.test('aaabbb'); // false
复制代码

否定查找 negated lookahead

括号内如果不匹配某个内容,则会有正则结果;反之无结果:

/aaa(?!\d+)/.exec('aaabbb'); // ["aaa"]
/aaa(?!\d+)/.exec('aaa123'); // null
/\d{4}(?!-d{2})+/.exec('2018-12-17'); // ["2018"]
/\d{4}-(?![a-z]+)/.test('2018-12-17'); // true
复制代码

一些 demo

中间考虑的比较少,以熟悉正则为主

获取 url params 参数

let data = location.search.substr(1); //name=eminoda&age=29
data.match(/([0-9A-Za-z]+)=([0-9A-Za-z]+)/g) //["name=eminoda", "age=29"]
复制代码

千分位(10000 => 10,000)

data = "12345678"
data.replace(/\d{1,3}(?=(\d{3})+(\.)?$)/g, '$&,');//"12,345,678"

// 稍微解释下
reg = /\d{1,3}(?=(\d{3})+(\.)?$)/g
// 当前匹配内容:12{345}(678) 大括号为正向匹配省略内容,括号为 \. 括号匹配内容
reg.exec(data)//["12", "678", undefined, index: 0, input: "12345678", groups: undefined]
// 当前匹配内容:345{}

(678) 大括号为正向匹配省略内容,括号为 \. 括号匹配内容
reg.exec(data)//["345", "678", undefined, index: 2, input: "12345678", groups: undefined]
复制代码

html 标签内部 key=value 获取 key

var excludeQuotesRe = /[^\s]*=(?:"([^\s]+)")/;
// ["id="app"", "app", index: 0, input: "id="app"", groups: undefined]
'id="app"'.match(excludeQuotesRe); 
复制代码

面试真要是非常恶心的正则,也就直接 gg 吧 -。-

手撕(代码实现)

模拟 instanceof 实现

需要了解原型相关概念

  • 获取实例对象的原型对象引用 _proto_
  • 和构造函数的原型对象引用作对比,若不成立,通过递归继续获取 _proto_ 再次比对
function myInstanceof(obj,Ctor){
    const prototype = Ctor.prototype
    const proto = Object.getPrototypeOf(obj); //获取原型对象 obj.__proto__
    if(proto){
      if(proto===prototype){
         return true;
      }else{
         return myInstanceof(proto,Ctor);
      }
    }else{
      return false;
    }
}
复制代码

模拟一个 new 操作符

需要了解原型相关概念

  • 在自定义的 new 方法中创建一个空的实例对象,处理后最后返回
  • 将实例对象的原型对象引用指向构造函数的原型对象
  • 通过 apply ,借用构造函数上的属性 or 方法
function myNew(Ctor){
  // 创建一个空对象,最后作为实例对象返回出去
  let instance = {};
  
  // 建立原型链关系
  if(Ctor.prototype){
    instance._proto_ = Ctor.prototype;
  }
  
  // 处理 Ctor 构造函数属性获取能力
  const args = Array.prototype.slice.call(arguments,1);
  Ctor.apply(instance,args);
  return instance;
}

function User(name){
  this.name = name;
}
const instance = myNew(User,'eminoda');
console.log(instance.name); // eminoda
复制代码

扁平数组转为树结构

如下,具有父子属性的对象一维数组 data,当然这个 data 我事先排号序了:

var data = [
  { pid: 0, id: 1, name: 'a' },
  { pid: 1, id: 2, name: 'a-1' },
  { pid: 1, id: 3, name: 'a-2' },
  { pid: 3, id: 4, name: 'a-2-1' },
  { pid: 3, id: 5, name: 'a-2-2' },
  { pid: 0, id: 6, name: 'b' },
  { pid: 6, id: 7, name: 'b-1' },
  { pid: 6, id: 8, name: 'b-2' },
];

const tree = data.reduce((acc, curr) => {
  if (!Array.isArray(acc)) {
    const tree = [];
    tree.push(acc);
    return appendNode(tree, curr);
  } else {
    return appendNode(acc, curr);
  }

  function appendNode(list, node) {
    for (let i = 0; i < list.length; i++) {
      const curNode = list[i];
      // 同级
      if (curNode.pid == node.pid) {
        list.push(node);
        return list;
      } else {
        if (isParentNode(curNode, node)) {
          if (!curNode.children) {
            curNode.children = [];
          }
          curNode.children.push(node);
          return list;
        } else {
          if (curNode.children) {
            return appendNode(curNode.children, node);
          }
        }
      }
    }
    return list;
  }

  function isParentNode(pNode, cNode) {
    return pNode.id === cNode.pid;
  }
});
复制代码

实现深拷贝

对于引用类型的数据递归再做拷贝操作

// 通过 json api 完成
var newObj = JSON.parse( JSON.stringify( someObj ) );

// 但面试时肯定会考察更多的知识点,so 会稍微复杂下
function deepCopy(obj) {
  function _isComplexType(obj) {
    return typeof obj === 'object';
  }
  let result;
  if (_isComplexType(obj)) {
    result = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
      const val = obj[key];
      result[key] = _isComplexType(val) ? deepCopy(val) : val;
    }
  } else {
    result = obj;
  }
  return result;
}

let data = { name: 'eminoda', skill: ['a', 'r', 'v'] };
let data2 = deepCopy(data);
data.skill.push('foo');
console.log(data2.skill); //["a", "r", "v"]
复制代码

模拟 call

在借用者中临时建个属性 tempFn,其引用为当前执行者 this 的引用,执行该 tempFn,最后删除该属性。

Function.prototype.myCall = function (...args) {
  const Ctor = args[0];
  args.shift();
  Ctor.tempFn = this;
  Ctor.tempFn(...args);
  delete Ctor.tempFn;
};

function User() {
  this.name = 'name is User';
}

function Foo() {
  this.name = 'name is Foo';
  this.say = function (...data) {
    console.log(this.name, data);
  };
}

const foo = new Foo();

foo.say.myCall(User, 'say1', 'say2');

复制代码

实现节流和防抖

节流

执行第一次后,舍弃某时间段内的后续执行请求,直至状态改变

将多次执行,分为固定时间间隔后再执行,用来 稀释 多次操作。比如:input search 输入查询

let num = 0;
function print() {
  console.log(++num);
}
复制代码


throttle 节流方法:

// 节流,默认 0.5 秒内的执行请求全部丢弃
function throttle(fn, time = 500) {
  let canRun = true;
  return function () {
    if (!canRun) return;
    canRun = false;
    setTimeout(() => {
      fn();
      canRun = true;
    }, time);
  };
}
复制代码


定义一个 setInterval 模拟用户每隔 0.1 秒的连续输入:

let count = 0;
let throttleFn = throttle(print);
let timer = setInterval(function search() {
  console.log(`search called`);
  count++;
  throttleFn();
  if (count == 10) {
    clearInterval(timer);
  }
}, 100);
复制代码


结果如下:

search called
search called
search called
search called
search called
1
search called
search called
search called
search called
search called
2
复制代码

防抖

忽略 持续不停 的操作,只采纳最后一次操作,区分真正需要执行的操作,比如:页面滚动超成的显示卡顿

延迟执行,执行时间前接受的执行请求将覆盖原先的执行请求

// 防抖
function debounce(fn, time = 1000) {
  let timer;
  return function () {
    timer && clearTimeout(timer);
    timer = setTimeout(function () {
      fn();
    }, time);
  };
}

let count = 0;
let debounceFn = debounce(print);
let timer = setInterval(function search() {
  console.log(`search called`);
  count++;
  debounceFn();
  if (count == 10) {
    clearInterval(timer);
  }
}, 100);
复制代码


执行效果:

search called
search called
search called
search called
search called
search called
search called
search called
search called
search called
search called
search called
1
复制代码

promise

我自己对 promise 源码实现理解不是很深刻(弱鸡),所以贴一个简单的实现(不含 promise 链式、all、race 等 feature):

function MyPromise(fn) {
  this.value = undefined;
  this.error = undefined;
  this.status = 'pending';
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];
  this.cashErrorCallbacks = [];

  this.resolveFn = function (value) {
    if (this.status == 'pending') {
      this.status = 'resolved';
      this.value = value;
      this.onFulfilledCallbacks.map((successFn) => successFn(this.value));
    }
  };
  this.rejectFn = function (error) {
    if (this.status == 'pending') {
      this.status = 'rejected';
      this.error = error;
      this.onRejectedCallbacks.map((errorFn) => errorFn(this.error));
    }
  };

  try {
    fn(this.resolveFn.bind(this), this.rejectFn.bind(this));
  } catch (err) {
    this.error = err;
    return this;
  }
}
MyPromise.prototype.then = function (successFn, errorFn) {
  if (this.status == 'pending') {
    this.onFulfilledCallbacks.push(successFn);
    this.onRejectedCallbacks.push(errorFn);
  }

  return this;
};
MyPromise.prototype.catch = function (errorCb) {
  if (this.status == 'pending' && this.error) {
    errorCb(this.error);
  }
};
new MyPromise((resolve, reject) => {
  setTimeout(function timer(params) {
    console.log('time is over');
    resolve('ok');
  }, 1000);
})
  .then(
    (data) => {
      console.log(data);
    },
    (error) => {
      console.log(error);
    }
  )
  .catch((err) => {
    console.log('catch', err);
  });
复制代码

实现一个洋葱模型 compose

参考 koa-compose 搞了一个:

var f1 = function (ctx, next) {
  console.log(1);
  next();
  console.log(4);
};
var f2 = function (ctx, next) {
  console.log(2);
  next();
  console.log(3);
};

function compose(middlewares, next) {
  return (ctx) => {
    return dispatch(0);
    function dispatch(i) {
      var fn = middlewares[i];
      if (!fn)
        return function none() {
          return null;
        };
      return fn(ctx, function () {
        return dispatch(i + 1);
      });
    }
  };
}

var chains = [f1, f2];
compose(chains)({ name: 'eminoda' });

//1
//2
//3
//4
复制代码

实现柯里函数 curry

注意 arguments 拿到的数据是个“假”数组,无法使用原生数组的一些 api

function curry(fn) {
  const originArgsCount = fn.length;
  let allArgs = [];
  return function execute() {
    allArgs = [...allArgs, ...arguments];
    if (allArgs.length === originArgsCount) {
      return fn.apply(null, allArgs);
    }
    return function () {
      return execute(...arguments);
    };
  };
}

function add(a, b, c) {
  return a + b + c;
}
const cFn = curry(add);

console.log(cFn(1)(2)(3)); //6
console.log(cFn(1, 2)(3)); //6

复制代码

参考文章

感谢如下这些文章的作者: