1. es5 实现继承
【 常用继承方式使用组合式继承、寄生式组合继承,具体场景具体使用 】
-
通过原型链继承:
- 就是让对象实例通过原型链的方式串联起来,当访问目标对象的某一属性时,能顺着原型链进行查找,从而达到类似继承的效果,让子类的构造函数的原型对象指向父类的实例对象
- 存在缺陷:
- 在创建子类实例的时候,不能向父类的构造函数中传递参数
- 原型中包含的引用值会在所有实例之间共享,修改一个实例,另一个实例会跟着修改
(不同子类实例,修改的同一个父类实例的值)
function Father() { this.colors = ['red', 'blue', 'green'] } function Son() { } Son.prototype = new Father() let instance1 = new Son() instance1.colors.push('black') console.log(instance1.colors) // "red,blue,green,black" let instance2 = new Son() console.log(instance2.colors) // "red,blue,green,black"
-
借用构造函数继承:
- 利用 call 调用父构造函数,且让 this 指向子构造函数
- 存在缺陷:
- 子类不能访问父类原型上定义的方法,所以必须在父构造函数中定义方法,因此函数不能复用
function Father(num) { this.num = num; } Father.prototype.test = function() { console.log(111); } function Son(num) { Father.call(this, num); this.n = num; } let instance = new Son(123) console.log(instance.n) // 123 console.log(instance.num) // 123 console.log(instance.test()) // instance.test is not a function
-
组合继承:
- 通过原型链继承来继承方法,借用构造函数继承来继承属性,这样函数可以复用,且子实例进行修改时,不会相互影响
- 存在缺陷:
- 组合式继承存在效率问题,父类构造函数会被调用两次,第一次在创建子类原型,第二次在子类构造函数中调用
- 最终导致子类原型包含父类所有的实例属性,浪费空间(因为属性在子实例上,方法在原型链上,所以子构造函数的原型对象不需要包含父类属性)
function Father(name) { this.name = name; this.colors = ["red", "blue", "green"]; } Father.prototype.sayName = function () { console.log(this.name); }; function Son(name, age) { Father.call(this, name); // 第二次调用 Father() this.age = age; } Son.prototype = new Father(); // 第一次调用 Father() subType.prototype.constructor = subType; Son.prototype.sayAge = function () { console.log(this.age); }; let instance1 = new Son("Nicholas", 29); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" instance1.sayName(); // "Nicholas"; instance1.sayAge(); // 29 let instance2 = new Son("Greg", 27); console.log(instance2.colors); // "red,blue,green" instance2.sayName(); // "Greg"; instance2.sayAge(); // 27
-
原型式继承:
- 在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制
【 新对象将 person 作为原型,所以它的原型中就包含一个基本类型值属性 name 和一个引用类型值属性 friends。这意味着 person.friends不仅仅属于 person 所有,而且也会被 anotherPerson 以及 yetAnotherPerson 共享。实际上,这就相当于又创建了 person 对象的两个副本 】 - 存在缺陷:
- 属性中包含的引用数值,会在对象之间共享,与原型链继承中引用数值会共享类似
function object(o) { function F() { } F.prototype = o return new F() } let person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; let anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); let yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(person.name); // "Nicholas" console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
- 在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制
-
寄生式继承:
- 将 object 封装,在函数中以某种方式增强这个继承后的对象
- 存在缺陷:给对象添加函数会导致函数难以重用,与构造函数模式类似
function object(o) { function F() { } F.prototype = o return new F() } function createAnother(original, sex) { let clone = object(original); // 通过调用函数创建一个新对象 clone.sex = sex; clone.sayHi = function () { // 以某种方式增强这个对象 console.log("hi"); }; return clone; // 返回这个对象 } let person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; let anotherPerson = createAnother(person, "famle"); console.log(anotherPerson.sex); // "famle" anotherPerson.sayHi(); // "hi"
-
寄生式组合继承:
- 寄生组合继承的模式是现在业内公认的比较可靠的 JS 继承模式,ES6 的 class 继承在 babel 转义后,底层也是使用的寄生组合继承的方式实现的
- 解决了上述几个方法的问题:
- 继承属性,靠的是借用构造函数继承,修改不会影响其他子实例
- 继承方法,指向一个 “空” 的构造函数的实例,这个空的构造函数的原型指向父构造函数的原型
【 就是在这里,优化了 “父类构造函数会被调用两次” 这个问题,不通过指向 new 父构造函数,间接让父构造函数的原型在原型链上,而是想直接指向。但为了让子实例可以加私有方法,所以 new 一个空构造函数,让这个空构造函数指向父构造函数的原型,这样就避免了 new 父构造函数时,重复运行父类构造函数 】
// 实现继承的核心函数 function inheritPrototype(subType, superType) { function F() {}; //F()的原型指向的是superType F.prototype = superType.prototype; //subType的原型指向的是F() subType.prototype = new F(); // 重新将构造函数指向自己,修正构造函数 subType.prototype.constructor = subType; } // 设置父类 function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; SuperType.prototype.sayName = function () { console.log(this.name) } } // 设置子类 function SubType(name, age) { //构造函数式继承--子类构造函数中执行父类构造函数 SuperType.call(this, name); // 只在这里运行了一次父类构造函数 this.age = age; } // 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费 inheritPrototype(SubType, SuperType) // 添加子类私有方法 SubType.prototype.sayAge = function () { console.log(this.age); } var instance = new SubType("Taec", 18) console.dir(instance)
2. es5 实现箭头函数
其实就是拿个变量保存上下文的 this(通常用 _this)
ES6 function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
ES5 function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
3. == 和 ===
-
==
-
用来进行一般比较检测两个操作数是否相等,可以允许进行类型转换
-
规则 1:NaN 和其他任何类型比较永远返回 false(包括和他自己)
-
规则 2:Boolean 和其他任何类型比较,Boolean 首先被转换为 Number 类型
-
规则 3:String 和 Number比较,先将 String 转换为 Number 类型
-
规则 4:null == undefined 比较结果是 true,除此之外,null,undefined 和其他任何结果的比较值都为 false
- 特殊规则,并不是转换为 Number 类型
Number(null); // 0 Number(undefined); // NaN
- 特殊规则,并不是转换为 Number 类型
-
规则 5:原始类型和引用类型做比较时,引用类型会依照 ToPrimitive 规则转换为原始类型
- ToPrimitive 规则,是引用类型向原始类型转变的规则,它遵循先 valueOf 后 toString 的模式期望得到一个原始类型
- 如果还是没法得到一个原始类型,就会抛出 TypeError
-
[ ] == ! [ ]
第一步,! [ ] 会变成 false
第二步,应用 规则2 ,题目变成: [ ] == 0
第三步,应用 规则5 ,[ ] 的 valueOf 是 0,题目变成: 0 == 0
所以, 答案是 true -
[ undefined ] == false
第一步,应用 规则5 ,[ undefined ] 通过 toString 变成 ‘’,
题目变成 ‘’ == false
第二步,应用 规则 2 ,题目变成 ‘’ == 0
第三步,应用 规则 3 ,题目变成 0 == 0
所以, 答案是 true
但是 if( [ undefined ] ) 又是个 true
-
-
===
- 用于严格比较,只要类型不匹配就返回flase
- null === null 和 undefined === undefined 都是返回 true
(虽然显而易见,但我在被面试官追问的时候,说了为 false,细节掌握的不自信了) - 不过 NaN === NaN 结果为 false
-
但是对于 Array,Object 等高级类型之间比较,== 和 === 是没有区别的
4. js 数据类型
es5 的时候,是 number,string,boolean,object,null,undefined
es6 的时候新增 symbol
es10 的时候新增 bigint
基本类型:number,string,boolean,null,undefined,symbol,bigint
引用类型:object (但这个时候并不是等着你只说 object,我就翻过车 )
引用类型包括 Object 类型,Array 类型,Function 类型,Map 类型,WeakMap 类型,WeakSet 类型,Set 类型,RegExp 类型等等
5. 深拷贝
(深拷贝需考虑特殊情况,undefined,function,symbol,相互引用)
- 通过 JSON.parse(JSON.stringify(obj))
- 但这种方式有局限性,当值为 undefined,symbol,Function 会在转换过程中被忽略
- 如果被拷贝的对象中,存在 bigint,会报错
- 如果被拷贝的对象中,存在循环引用的时候,会报错
let obj = { } obj.a = obj JSON.parse(JSON.stringify(obj)) // 会报错
- 如果 obj 里面存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串
- 如果 obj 里有 Set,Map,RegExp,Error 对象,则序列化的结果将只得到空对象,如果直接 JSON.parse(JSON.stringify( )) 这些对象的时候,得到的也只是空对象,但是可以拷贝 Array 数组
- 如果 obj 里有 NaN、Infinity 和 -Infinity,则序列化的结果会变成 null
- 不能拷贝函数,JSON.stringify(function) 为 undefined
- 所以还得自己实现递归函数进行深拷贝
- 先考虑各种特殊情况
- 拷贝函数
function fn(a, b, c) { return a * b * c; } // 方式 1,很多函数库都是用这个方法 var newFn = new Function('return ' + fn.toString())(); // 方式 2,利用 bind 返回函数,如果 bind 没有参数,或者传入的为 null,则指向 window var newFn = fn.prototype.bind();
- 拷贝 symbol
// 方法 1,返回对象身上全部 symbol 类型组成的数组 Object.getOwnPropertySymbols(…) // 方法 2, 获取所有的键,同时包括 Symbol,但不会获取深拷贝原型链上的数据 Reflect.ownKeys(…)
- 解决循环引用
- 用 map 存各个值或引用,如果碰到重复,直接返回引用
- 拷贝函数
- 先考虑各种特殊情况
6. 强引用 和 弱引用
- 引用数据类型,即引用类型在栈中存储了指针,这个指针指向堆内存中的地址,真实的数据存放在堆内存里
- 强引用
- 将一个引用类型数据通过变量或常量保存,这个变量或常量就是强引用
- 如果存在强引用指向堆内存,则堆内存不会被当作垃圾回收掉
// test 就是强引用 let test = { a: 1, b: 2 }
- 弱引用
- WeakMap 的键只能是弱引用,WeakSet 的值只能是弱引用,WeakRef 传入对象,返回其弱引用
- 如果存在弱引用指向堆内存,但没有强引用指向堆内存,则堆内存仍然会被当做垃圾回收掉
- 但是注意,并不是说,没有强引用指向,就直接垃圾回收了,垃圾回收是懒回收,当觉得你这个占用内存过多,影响到运行,才会值行垃圾回收,否则频繁执行垃圾回收,会阻塞
let a = new Set(); let b = new WeakSet(); for(let i = 0; i < 100; i++) { a.add({}); b.add({}); } console.log(a); console.log(b); // 此时 b 内还有 100 个元素,都没有被回收
let a = new Set(); let b = new WeakSet(); for(let i = 0; i < 10000; i++) { a.add({}); b.add({}); } console.log(a); console.log(b); // 此时 b 内没有元素,全部都被回收了
7. WeakMap,WeakSet,WeakRef
WeakMap
- 与 Map 的 api 类似,且其中的键都是唯一的
- 与 Map 的区别为
- 因为弱引用,WeakMap 的键是不可枚举的(没有方法能给出所有的键),如果键是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果
- WeakMap 的键只能是 Object 类型,而不能像 Map 那样,可以是任何类型的任意值
- 键为弱引用,如果没有其他的对 WeakMap 中键的引用,那么这些键值对会被垃圾回收掉
WeakSet
- 与 Set 的 api 类似,且其中的元素都是唯一的
- 与 Set 的区别为
- 因为弱引用,WeakSet 的元素是不可枚举的(没有方法能给出所有的元素),如果元素是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果
- WeakSet 只能是 Object 的集合,而不能像 Set 那样,可以是任何类型的任意值
- 集合中对象的引用为弱引用,如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被垃圾回收掉
WeakRef
- new WeakRef()
WeakRef 对象包含对对象的弱引用let ref = new WeakRef(targetObject) // ref 即是对 targetObject 的弱引用
- WeakRef.prototype.deref()
deref 方法返回 WeakRef 实例的目标对象,如果目标对象已被垃圾收集,则返回 undefinedlet obj = ref.deref(); // 这个 obj 是强引用