前言
开头 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 原型对象
就这样他们一级一级引用上去,直至顶级的 Object 原型对象。画张图,将是这个样子:
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 的实例:
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
参考文章
感谢如下这些文章的作者:
- 【掘金】2万字 | 前端基础拾遗90问
- 【掘金】前端面试-手撕代码篇
- 【掘金】探寻 JavaScript 精度问题以及解决方案
- 【掘金】「中高级前端面试」JavaScript手写代码无敌秘籍
- 【掘金】JavaScript 专题之函数柯里化
- 【头条】0.1+0.2≠0.3?聊聊 js 中的双精度浮点数
- 【头条】js 基础:如何自定义一个 new 操作符来创建对象
- 【慕课网】IEEE 754浮点数标准中64位浮点数为什么指数偏移量是1023?
- 【MDN】isPrototypeOf
- 【MDN】instanceof
- 【MDN】flatMap
- 【MDN】flat
- 【github】js 基础 -- 面向对象 4.几种继承方式
- 【github】js 基础 -- this
- 【github】js 那些"奇技淫巧"