JS 常考进阶面试题
在这一章节中咱们持续来理解 JS 的一些常考和容易凌乱的根底知识点。
== vs ===
涉及面试题:== 和 === 有什么区别?
对于 ==
来说,如果比照单方的类型 不一样 的话,就会进行 类型转换,这也就用到了咱们上一章节讲的内容。
如果咱们须要比照 x
和 y
是否雷同,就会进行如下判断流程:
- 首先会判断两者类型是否 雷同。雷同的话就是比大小了
- 类型不雷同的话,那么就会进行类型转换
- 会先判断是否在比照
null
和undefined
,是的话就会返回true
-
判断两者类型是否为
string
和number
,是的话就会将字符串转换为number
1 == '1' ↓ 1 == 1
-
判断其中一方是否为
boolean
,是的话就会把boolean
转为number
再进行判断'1' == true ↓ '1' == 1 ↓ 1 == 1
-
判断其中一方是否为
object
且另一方为string
、number
或者symbol
,是的话就会把object
转为原始类型再进行判断'1' == {name: 'yck'} ↓ '1' == '[object Object]'
思考题:看完了下面的步骤,对于 [] == ![] 你是否能正确写出答案呢?
如果你感觉记忆步骤太麻烦的话,我还提供了流程图供大家应用:
当然了,这个流程图并没有将所有的状况都列举进去,我这里只将罕用到的状况列举了,如果你想理解更多的内容能够参考 规范文档。
对于 ===
来说就简略多了,就是判断两者类型和值是否雷同。
闭包
涉及面试题:什么是闭包?
要了解闭包,首先必须了解 Javascript 非凡的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
Javascript 语言的非凡之处,就在于函数外部能够间接读取全局变量。
而闭包却是可能读取其余函数外部变量的函数。所以,在实质上,闭包就是将函数外部和函数内部连接起来的一座桥梁。
闭包的特点
1. 函数嵌套函数
2. 函数外部能够援用内部的参数和变量
3. 参数和变量不会被垃圾回收机制回收
因而闭包常会被用于
1 能够贮存一个能够长期驻扎在内存中的变量
2. 防止全局变量的净化
3. 保障公有成员的存在
那闭包又因为什么起因不被回收呢
简略来说,js 引擎的工作分两个阶段,
一个是语法查看阶段,
一个是运行阶段。而运行阶段又分预解析和执行两个阶段。
在预解析阶段,先会创立执行上下文,执行上下文又包含变量对象、变量对象的作用域链和 this 指向的创立。
创立执行上下文后,会对变量对象的属性进行填充。
进入执行代码阶段,此时执行上下文有个 Scope 属性
该属性作为一个作用域链蕴含有该函数被定义时所有外层的变量对象的援用
js 解析器逐行读取并执行代码时
当咱们须要查问内部作用域的变量时,其实就是沿着作用域链,顺次在这些变量对象里遍历标志符,直到最初的全局变量对象。
基于 js 的垃圾回收机制: 在 Javascript 中,如果一个对象不再被援用,那么这个对象就会被 GC 回收。如果两个对象相互援用,而不再被第 3 者所援用,那么这两个相互援用的对象也会被回收。因为函数 a 被 b 援用,b 又被 a 外的 c 援用,所以定义了闭包的函数尽管销毁了,然而其变量对象仍然被绑定在函数上,只有仍被援用,变量会持续保留在内存中,这就是为什么函数 a 执行后不会被回收的起因。
变量对象 VO:var 申明的变量、function 申明的函数,及以后函数的形参
作用域链:以后变量对象 + 所有父级作用域 [[scope]]
this 值:在进入执行上下文后不再扭转
PS:作用域链其实就是一个变量对象的链,函数的变量对象称之为 active object,简称 AO。函数创立后就有动态的 [[scope]] 属性,直到函数销毁)创立执行上下文后,会对变量对象的属性进行填充。所谓属性,就是 var、function 申明的标志符及函数形参名,至于属性对应的值:变量值为 undefined,函数值为函数定义,形参值为实参,没有传入实参则为 undefined。
三、闭包的微观世界
如果要更加深刻的理解闭包以及函数 a 和嵌套函数 b 的关系,咱们须要引入另外几个概念:函数的执行环境(excution context)、流动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数 a 从定义到执行的过程为例论述这几个概念。
3 当定义函数 a 的时候,js 解释器会将函数 a 的作用域链 (scope chain) 设置为定义 a 时 a 所在的“环境”,如果 a 是一个全局函数,则 scope chain 中只有 window 对象。
4 当执行函数 a 的时候,a 会进入相应的执行环境(excution context)。
5 在创立执行环境的过程中,首先会为 a 增加一个 scope 属性,即 a 的作用域,其值就为第 1 步中的 scope chain。即 a.scope= a 的作用域链。
6 而后执行环境会创立一个流动对象(call object)。流动对象也是一个领有属性的对象,但它不具备原型而且不能通过 JavaScript 代码间接拜访。创立完流动对象后,把流动对象增加到 a 的作用域链的最顶端。此时 a 的作用域链蕴含了两个对象:a 的流动对象和 window 对象。
7 下一步是在流动对象上增加一个 arguments 属性,它保留着调用函数 a 时所传递的参数。
8 最初把所有函数 a 的形参和外部的函数 b 的援用也增加到 a 的流动对象上。在这一步中,实现了函数 b 的的定义,因而如同第 3 步,函数 b 的作用域链被设置为 b 所被定义的环境,即 a 的作用域。
到此,整个函数 a 从定义到执行的步骤就实现了。此时 a 返回函数 b 的援用给 c,函数 b 的作用域链又蕴含了对函数 a 的流动对象的援用,也就是说 b 能够拜访到 a 中定义的所有变量和函数。函数 b 被 c 援用,函数 b 又依赖函数 a,因而函数 a 在返回后不会被 GC 回收。
当函数 b 执行的时候亦会像以上步骤一样。因而,执行时 b 的作用域链蕴含了 3 个对象:b 的流动对象、a 的流动对象和 window 对象,如下图所示:
如图所示,当在函数 b 中拜访一个变量的时候,搜寻程序是:
9 先搜寻本身的流动对象,如果存在则返回,如果不存在将持续搜寻函数 a 的流动对象,顺次查找,直到找到为止。
10 如果函数 b 存在 prototype 原型对象,则在查找完本身的流动对象后先查找本身的原型对象,再持续查找。这就是 Javascript 中的变量查找机制。
11 如果整个作用域链上都无奈找到,则返回 undefined。
小结,本段中提到了两个重要的词语:函数的定义与执行。文中提到函数的作用域是在定义函数时候就曾经确定,而不是在执行的时候确定(参看步骤 1 和 3)。用一段代码来阐明这个问题:
<script>
function f(x) {var g = function() {alert(++x);
}
return g;
}
var h = f(1);
h(); // alert 2
h(); // alert 2
</script>
· 假如函数 h 的作用域是在执行 alert(h())确定的,那么此时 h 的作用域链是:h 的流动对象 ->alert 的流动对象 ->window 对象。这段代码中变量 h 指向了 f 中的那个匿名函数(由 g 返回)。
· 假如函数 h 的作用域是在定义时确定的,就是说 h 指向的那个匿名函数在定义的时候就曾经确定了作用域。那么在执行的时候,h 的作用域链为:h 的流动对象 ->f 的流动对象 ->window 对象。
如果第一种假如成立,那输入值就是 undefined;如果第二种假如成立,输入值则为 1。
运行后果证实了第 2 个假如是正确的,阐明函数的作用域的确是在定义这个函数的时候就曾经确定了。
(转载请注明出处:http://www.felixwoo.com/archi…
四、闭包的利用场景
12 爱护函数内的变量平安。以最开始的例子为例,函数 a 中 i 只有函数 b 能力拜访,而无奈通过其余路径拜访到,因而爱护了 i 的安全性。
13 在内存中维持一个变量。仍然如前例,因为闭包,函数 a 中 i 的始终存在于内存中,因而每次执行 c(),都会给 i 自加 1。
14 通过爱护变量的平安实现 JS 公有属性和公有办法(不能被内部拜访)举荐浏览:http://javascript.crockford.c…
<script>
function constructor() {
var this = this;
var membername = value;
function membername(...) {...}
}
</script>
五、Javascript 的垃圾回收机制
在 Javascript 中,如果一个对象不再被援用,那么这个对象就会被 GC 回收。如果两个对象相互援用,而不再被第 3 者所援用,那么这两个相互援用的对象也会被回收。因为函数 a 被 b 援用,b 又被 a 外的 c 援用,这就是为什么函数 a 执行后不会被回收的起因。
在 JS 中,闭包存在的意义就是让咱们能够间接拜访函数外部的变量。
原型
涉及面试题:如何了解原型?如何了解原型链?
1. 每个对象都有__proto__属性
,该属性指向其构造函数的原型对象,__proto__
将对象和其原型对象连接起来组成原型链
2. 在调用实例的办法和属性时,如果在实例对象上找不到,就会往原型对象上找
3. 构造函数的 prototype 属性
也指向实例的原型对象
4. 原型对象的 constructor 属性
指向构造函数。
继承
说到继承,最容易想到的是 ES6 的extends
,当然如果只答复这个必定不合格,咱们要从函数和原型链的角度上实现继承,上面咱们一步步地、递进地实现一个合格的继承
实现一个办法能够从而实现对父类的属性和办法的继承,解决代码冗余反复的问题
一. 原型链继承
原型链继承的原理很简略,
间接让子类的原型对象指向父类实例,
Child.prototype=new Parent()
当子类实例找不到对应的属性和办法时,就会往它的原型对象,也就是父类实例上找,
从而实现对父类的属性和办法的继承
原型继承的毛病:
1. 因为所有 Child 实例原型都指向同一个 Parent 实例, 因而对某个 Child 实例的父类援用类型变量批改会影响所有的 Child 实例
2. 在创立子类实例时无奈向父类结构传参, 即没有实现 super()的性能
二. 构造函数继承
构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的 this,
让父类的构造函数把成员属性和办法都挂到子类的 this 下来;
在 Child 的构造函数中执行
Parent.apply(this, arguments);
这样既能防止实例之间共享一个原型实例,又能向父类构造方法传参;
js 继承的形式继承不到父类原型上的属性和办法
构造函数继承的毛病:
1. 继承不到父类原型上的属性和办法
三. 组合式继承
既然原型链继承和构造函数继承各有互补的优缺点, 那么咱们为什么不组合起来应用呢, 所以就有了综合二者的组合式继承
-
Child.prototype=new Parent() Child.prototype.constructor=Child // 相当于在 Child 的构造函数中给 Parent 绑定 this
组合式继承的毛病:
1. 每次创立子类实例都执行了两次构造函数 (Parent.call() 和 new Parent()),尽管这并不影响对父类的继承,但子类创立实例时,原型中会存在两份雷同的属性和办法,这并不优雅
四. 寄生式组合继承
为了解决组合式继承中构造函数被执行两次的问题,
咱们将指向父类实例改为指向父类原型, 减去一次构造函数的执行
到这里咱们就实现了 ES5 环境下的继承的实现,这种继承形式称为寄生组合式继承。
Function.prototype.extend = function (supClass) {// 创立一个两头代替类, 避免屡次执行父类 (超类) 的构造函数
function F() {}
// 将父类的原型赋值给这个两头代替类
F.prototype = supClass.prototype;// 将原子类的原型保留
var proto = subClass.prototype;// 将子类的原型设置为两头代替类的实例对象
subClass.prototype = new F();// 将原子类的原型复制到子类原型上, 合并超类原型和子类原型的属性办法
// Object.assign(subClass.prototype,proto);var names = Object.getOwnPropertyNames(proto);for (var i = 0;i < names.length;i++) {var desc = Object.getOwnPropertyDescriptor(proto, names[i]);Object.defineProperty(subClass.prototype, names[i], desc);}
// 设置子类的构造函数时本身的构造函数, 以避免因为设置原型而笼罩构造函数
subClass.prototype.constructor = subClass;// 给子类的原型中增加一个属性, 能够快捷的调用到父类的原型办法
subClass.prototype.superClass = supClass.prototype;// 如果父类的原型构造函数指向的不是父类构造函数, 从新指向
if (supClass.prototype.constructor !== supClass) {supClass.prototype.constructor = supClass;}
}
function Ball(_a) {this.superClass.constructor.call(this, _a);}
Ball.prototype.play = function () {this.superClass.play.call(this);// 执行超类的 play 办法
console.log("end");}
Object.defineProperty(Ball.prototype, "d", {value:20})
Ball.extend(Box);var b=new Ball(10);console.log(b);
是目前最成熟的继承形式,babel 对 ES6 继承的转化也是应用了寄生组合式继承
咱们回顾一下实现过程:
- 原型链继承,通过把子类实例的原型指向父类实例来继承父类的属性和办法;但缺点在于,对子类实例继承的援用类型的批改会影响到所有的实例对象以及无奈向父类的构造方法传参。
- 因而咱们引入了构造函数继承, 通过在子类构造函数中调用父类构造函数并传入子类 this 来获取父类的属性和办法,但缺点在于,构造函数继承不能继承到父类原型链上的属性和办法。
- 综合了两种继承的长处,提出了组合式继承,但组合式继承也引入了新的问题,它每次创立子类实例都执行了两次父类构造方法,
-
咱们通过将子类原型指向父类实例改为子类原型指向父类原型的浅拷贝来解决这一问题,也就是最终实现 —— 寄生组合式继承
深浅拷贝
涉及面试题:什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?
在上一章节中,咱们理解了对象类型在赋值的过程中其实是复制了地址,从而会导致扭转了一方其余也都被扭转的状况。通常在开发中咱们不心愿呈现这样的问题,咱们能够应用浅拷贝来解决这个状况。
let a = {age: 1}
let b = a
a.age = 2
console.log(b.age) // 2
浅拷贝 开展运算符 ...
来实现浅拷贝和 Object.assign({}, a)
首先能够通过 Object.assign
来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign
只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。
let a = {age: 1}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
另外咱们还能够通过开展运算符 ...
来实现浅拷贝
let a = {age: 1}
let b = {...a}
a.age = 2
console.log(b.age) // 1
通常浅拷贝就能解决大部分问题了,然而当咱们遇到如下状况就可能须要应用到深拷贝了
let a = {
age: 1,
jobs: {first: 'FE'}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native
浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有雷同的地址。要解决这个问题,咱们就得应用深拷贝了。
深拷贝
这个问题通常能够通过 JSON.parse(JSON.stringify(object))
来解决。
let a = {
age: 1,
jobs: {first: 'FE'}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
然而该办法也是有局限性的:
- 会疏忽
undefined
- 会疏忽
symbol
- 不能序列化函数
- 不能解决循环援用的对象
- 在遇到函数、
undefined
或者symbol
的时候,该对象也不能失常的序列化 - 原型链如何解决
- DOM 如何解决
- Date
- Reg
- ES6 类
- null
- boolen
- array
- string
- number
实现一个深拷贝是很艰难的,须要咱们思考好多种边界状况,比方原型链如何解决、DOM 如何解决等等,所以这里咱们实现的深拷贝只是简易版,并且我其实更举荐应用 lodash 的深拷贝函数。
function deepClone(obj) {function isObject(o) {return (typeof o === 'object' || typeof o === 'function') && o !== null
}
if (!isObject(obj)) {throw new Error('非对象')
}
let isArray = Array.isArray(obj)
let newObj = isArray ? [...obj] : {...obj}
Reflect.ownKeys(newObj).forEach(key => {newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return newObj
}
let obj = {a: [1, 2, 3],
b: {
c: 2,
d: 3
}
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2
谢巨匠
class Box {static ARG = ["a", "b"]
constructor(a1, b1) {
this.a = a1
this.b = b1
}
play() {console.log(this.a1 + this.b1)
}
}
var obj = {
a:1,
b:"a",
c:false,
d:{
e:undefined,
f:null,
g:[1, 2, 3, 4, 5],
h:new Date(),
i:/^[a-z]{2,4}$/gi,
j:new Box(4, 5),
k:{}}
}
Object.defineProperties(obj.d.k, {l:{ value:10},
m:{
configurable:true,
writable:true,
value:20
},
n:{
enumerable:true,
value:function() {console.log("aaaa")
}
},
o:{value:new Image()
}
})
function cloneObject(target, source) {var names = Object.getOwnPropertyNames(source)
for (let i = 0 i < names.length i++) {var desc = Object.getOwnPropertyDescriptor(source, names[i])
if (typeof desc.value === "object" && desc.value !== null) {
var obj
if (desc.value instanceof HTMLElement) {obj = document.createElement(desc.value.nodeName)
} else {switch (desc.value.constructor) {
case Box:
obj = new desc.value.constructor(desc.value[Box.ARG[0]], desc.value[Box.ARG[1]])
break
case RegExp:
obj = new desc.value.constructor(desc.value.source,desc.value.flags)
break
default :
obj = new desc.value.constructor()}
}
cloneObject(obj, desc.value)
Object.defineProperty(target, names[i], {
value:obj,
enumerable:desc.enumerable,
writable:desc.writable,
configurable:desc.configurable
})
} else {Object.defineProperty(target, names[i], desc)
}
}
return target
}
var obj1 = cloneObject({}, obj)
obj.d.k.m = 100
console.log(obj1)
new 操作符调用构造函数具体做了什么?
如下:
•创立一个新的对象;
•将构造函数的 this 指向这个新对象;
•为这个对象增加属性、办法等;
•最终返回新对象;