面试笔记-JavaScript
JavaScript
变量类型
JavaScript 变量类型分为值类型,引用类型;值类型存放在栈中,引用类型存放在堆中。
其中值类型有 7 种:
String
Boolean
Number
Null
Undefined
Symbol
BigInt
引用类型统称为Object
,又可细分为以下 3 种:
Object
Array
Function
typeof
可以通过typeof
关键字来判断一个变量的类型:
1 | typeof 'nick' // 'string' |
typeof
对于值类型的null
会判断为object
,其他的则是原始类型;对引用类型的function
会判断为function
,其他的则都为object
。
类型转换
字符串拼接
当 JavaScript 中变量遇到字符串时,会自动转换为字符串:
1 | var str = 123 + '00' |
相等和全等
使用相等符号==
对变量进行比较时,会进行类型转换,然后确定两个变量是否相等。
在转换变量类型是,会按照以下规则进行转换:
- 如果任意变量为布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为 1。
- 如果一个变量是字符串,另一个为数值,则尝试将字符串转换为数值,在比较是否相等。
- 如果一个变量为对象,另一个不是,则调用对象的 valueOf()方法获取其原始值,再根据前面的规则比较。
null
和undefined
相等。null
和undefined
不能转换为其他类型在进行比较。- NaN 与任何其他类型比较都返回 false,包括自己本身;不相等咋返回 true。
- 对象与对象比较,则会比较两个对象是否指向同一个对象的引用,不等则返回 false。
1 | null == undefined // true |
使用全等符号===
进行比较时,变量不会进行类型转换;也就是说,数据类型不同,就返回 false。
1 | null === undefined // false |
NaN 与任意值进行全等比较依然返回 false,包括自身。
if 条件,逻辑运算
使用到 if 语句以及逻辑运算时,条件内的值最终都会转换为布尔值。
原型和原型链
prototype
在 JavaScript 中,每个函数都会创建一个prototype
属性,这个属性是一个对象,里面的属性与方法可以在实例中共享。prototype
属性是通过调用构造函数穿件的对象的原型;也就是说,构造函数的原型就是prototype
属性。
1 | function Person() {} |
__proto__
实例对象查找属性时会从自身开始查找,如果有,则直接使用;如果没有,则会通过__proto__
找到构造对象的原型中查找。
1 | function Person() {} |
constructor
构造函数的prototype
和该构造函数的实例对象的__proto__
都有一个constructor
属性,默认都指向该构造函数本身。
1 | function Person() {} |
构造函数的prototype
的__proto__
构造函数的prototype
也有__proto__
属性,普通构造函数原型上的__proto__
指向特殊构造函数Object
的原型;而Object
原型上的__proto__
指向null
。
也就是说,实例对象查找属性时会从自身开始查找,如果没有,则会通过__proto__
一直查找原型中有没有需要的属性,有的话就取出,没有的话,就一直到null
,返回undefined
。
1 | function Person() {} |
构造函数的__proto__
所有构造函数都是内置构造函数Function
的实例,所以构造函数的__proto__
都指向Function
的原型,包括Function
自身的__proto__
,也是指向自己的原型。
1 | function Person() {} |
instanceof
基于原型链实现
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。也可以理解为构造函数的prototype
属性是否与实例的__proto__
相等。
实例对象的
__proto__
指向创建它的构造函数的原型。
构造函数原型的__proto__
指向Object
的原型。
构造函数都是Function
的实例。
作用域与闭包
作用域
JavaScript 中有三种作用域:全局作用域,函数作用域,块级作用域。
全局作用域
顾名思义,定义在全局中的变量所处的作用域环境就是全局作用域。全局作用域中的变量可以被任意访问。
1 | let name = 'nick' |
函数作用域
顾名思义,所处环境在函数中。函数作用域只能被当前作用域及内部作用域访问,无法被外部作用域访问。
1 | function foo() { |
块级作用域
包含在代码块{}
中的变量,所处环境就为块级作用域。块级作用域只能被当前作用域内访问,不能被外部访问(var 除外)。
1 | { |
闭包
闭包是指能够访问自由变量的函数,而自由变量则是值在当前函数作用域中未定义,但是却被使用的变量。
1 | var name = 'nick' |
对于自由变量的查找,是在函数定义的地方查找,而不是执行时。
1 | var name = 'nick' |
一般来说,有两种情况通常形成闭包:
- 函数作为返回值
- 函数作为参数
并且上面两种情况的函数都保留了对外部作用域中变量的引用,这就形成了闭包。
闭包的使用场景
setTimeout
原生的setTimeout
函数的第一个参数不能携带参数,可通过闭包实现。
1 | function getName(name) { |
封装私有变量
1 | function ownerVarible() { |
JavaScript 创建对象方式
对象字面量
简单粗暴
1 | let person = { |
工厂模式
工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来 达到复用的目的。
1 | function createPerson(name, age) { |
工厂模式能够解决创建多个类似对象的问题,但是没有解决对象标识问题,无法反应新创建的对象是什么类型。
构造函数模式
JavaScript 中每一个函数都可以作为构造函数,只要一个函数是通过 new
来调用的, 那么我们就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数 的 prototype
属性,然后将执行上下文中的 this
指向这个对象,最后再执行整个函数,如果返回值不是 对象,则返回新建的对象。
1 | function Person(name, age) { |
构造函数模式和工厂模式大致相同,但是有以下几点不同:
没有显示地创建对象。
属性和方法直接赋值给了this
。
没有return
。
构造函数解决了对象类型识别的问题,但是内部定义的方法会在每个实例上都重新创建一遍,造成浪费。
Object.create
该方法非常有用,因为它允许你为创建的对象选择一个原型对象,而不用定义构造函数,并且使用现有的对象来作为新创建对象的__proto__
。
1 | const person = { |
原型模式
每一个函数都有一个 prototype
属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此我们可以使用原型对象来添加公用属性和方法,从而实现代码的复用。
1 | function Person(name, age) { |
原型模式中,动态属性可以放在构造函数内,通过参数获取;共用的方法可以放在原型上,在实例间共享,重复使用。
但是原型模式的问题也在于所有属性都能够在实例之间共享。这似乎没什么问题,但是原型中若是有引用类型的属性,则会造成混乱。
继承
原型链继承
主要继承方式。通过原型继承多个引用类型的属性和方法。
1 | function Person(name, age) { |
原型链继承的缺点和之前通过原型模式创建对象一样,属性在实例间共享的问题。若是引用类型的属性,对其修改后,会影响到其他实例的属性。
1 | function BaseColors() { |
还有一个问题是,无法在创建子类实例是给父类传递参数。
盗用构造函数
基本思路:在子类构造函数中调用父类构造函数,通过apply
和call
改变this
指向的问题。
1 | function BaseColors() { |
利用call
或者apply
可以在创建子类实例的时候,将父类this
的值指向子类的实例;这样,就等于每个子类对象都有属于自己的属性,不会造成修改冲突。
盗用构造函数同样有缺点,也就是使用构造函数模式自定义类型时,必须在构造函数内定义方法,因此函数不能重用。并且,子类也无法访问父类原型上的方法,因此所有类型只能使用构造函数模式。
组合继承
组合继承也叫经典继承,结合了原型链与盗用构造函数的有点。基本思路是:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。
1 | function Person(name) { |
组合继承将子类的原型改为父类的实例,并且在子类构造函数中改变父类构造函数的指向,使其指向子类的实例。这样,子类不仅可以使用父类原型上的方法,也可以拥有自己的属性,并且可以向父类传递参数。
寄生式继承
寄生式继承的思想是:接收一个对象,然后克隆这个对象,对其进行增强,然后返回增强后的对象。
1 | function createEnhanceObject(obj) { |
寄生式组合继承
组合继承存在效率问题:父类构造函数始终都会被调用两次,一次是在创建子类原型时调用,一次是在子类构造函数中调用。
寄生式组合继承,在就利用到了寄生式继承的特点,通过一个增强函数做到只调用一次父类构造函数。
1 | function enhance(superType, subType) { |
通过enhance
函数,便只调用了一次父类构造函数,效率更高,并且原型链任然保持不变。因此,寄生式组合继承可以算是引用类型继承的最佳模式。
事件循环(event loop)
JavaScript 代码由上而下执行,遇到同步代码直接放入主线程,遇到异步代码放进异步队列。在异步队列中,宏任务(如setTimeout
,setInterval
)会放进宏任务队列,微任务(如Promise
)则放进微任务队列。
当主线程中的同步代码执行完毕之后,会进入异步队列中读取异步代码,首先将微任务放进主线程中执行,所有微任务执行完毕之后,就会将宏任务放进主线程执行。
不断重复上面的步骤。