前言
本篇主要归纳在面试中关于JavaScript高频考点,如有不对欢迎留言讨论。
JavaScript基础
一、数据类型
1.类型的种类及区别
- 基本数据类型: Number、String、Boolean、Undefined、Null、Symbol(ES6新增)、BigInt(ES10新增)
- 引用数据类型: 包含以下三类
- 1.基本引用类型:Object、Array、Function、Date、RegExp
- 2.基本包装类型:String、Number、Boolean
- 3.单体内置对象:Global、Math
区别:
- 基本数据类型按值访问,不能添加属性和方法,引用数据类型按引用访问,可以添加属性和方法
- 基本数据类型标识符和值保存在栈中,引用数据类型内容保存在堆中,在栈中保存对应内容的引用地址和变量标识。
- 栈内存自动分配内存,堆内存动态分配内存,不会自动释放,需要手动设置为null
1.1 null==undefined?
在ECMAScript规范中,null和undefined行为类似,都表示无效的值,但是不全等,因为全等是先比较类型再比较值,二者类型本就不同。
null==undefined//true
null===undefined//false
1.2 ![][],[][]
![]==[]//true
在“==”号的比较中,左右会进行隐式转换。
- 左侧![]中!的关系会先强制将![]转为false
- 右侧[]进行隐式转换,空数组也属于对象,对象隐式转换有以下几个步骤
- 先进行valueOf,有原始值就直接返回
- 没原始值则进行.toString,有原始值就直接返回
- 没原始值就直接抛出错误
而[].valueOf=[],[].toString()=’’;
则左边false==’'返回true
[]==[]//false
这个就不要绕进去了,两个空数组作为引用类型它们的引用地址不同,直接返回false
1.3 0.1+0.2=0.3?
在js中浮点数是通过64位二进制来进行表示的,其中第1位为符号位,2-12位表示表示指数位,剩下52位表示尾数位。
0.1转为二进制是一个无线循环数:0.0001100110011001100…
,而只取52位尾数位,造成了精度丢失,再转为十进制就不等于原来的0.1了,所以相加也不等于0.3
2.为什么需要栈和堆两个空间
因为在JavaScript中需要通过栈来维护程序执行期间上下文的状态,而为了不影响上下文切换的执行效率,所以不允许栈空间太大,因此需要堆来存储占用空间较大的引用类型。
3.什么是包装类型
我们都知道基本数据类型是不能有属性和方法的,但是我们能发现Number、String、Boolean类型却能调用像toString的这种方法,原因是因为它们的包装对象。
特点: 包装对象只存在执行的瞬间,执行完则立刻销毁。
如:
let num=123;
let numStr=num.toString();
等同于
let num=123;
let obj=new Number(num);
let numStr=obj.toString();
obj=null;//立刻销毁
怎么在包装类型上自定义属性方法使用呢?
从上面可以知道其实就是暂时引用了包装对象上的属性方法,而包装对象也是对象,所以我们在其原型上添加属性方法就行了。
let num = 123;
Number.prototype.a = 'xy'
Number.prototype.eat = function () {
return '吃饭'
}
console.log(num.a);//xy
console.log(num.eat());//吃饭
4.类型的判断方式
1.typeof操作符
- 通过typeof操作符来对机器码前三位进行判定。
- 举例: typeof 123 或 typeof 123===‘number’
- 返回值: number、string、boolean、object、undefined、symbol、function字符串或布尔值。
- 优点: 可以对除了null的基本数据类型做出准确的判断
- 缺点: 不能判断null类型,对待引用数据类型都返回object不能准确判断其类型
2.instanceof
- 通过原型链查找的方式判断构造函数的原型是否出现在这个实例对象的原型链上
- 举例: [] instanceof Array
- 返回值类型: boolean
- 优点: 可以判断某一对象的具体类型
- 缺点: 不能用来判断基本数据类型、对于多个窗口的多全局环境不适用
3.constructor
function A(){}
function B(){}
const a=new A()
//原型链继承: A.prototype=new B(),此时A.constructor=B
- 通过内部的constructor属性判断是其构造函数
- 举例: a.constructor===A
- 返回值类型: boolean
- 优点: 可以判断其构造函数
- 缺点: 对象原型链继承后,判断不准确
4.Object.prototype.toString.call(推荐)
- 通过Object原型上的toString方法可返回其对应类型
- 举例: Object.prototype.toString.call(“abc”)
- 返回值: [object ***]字符串
- 优点: 原始类型、引用类型都能准确判断,也不存在多全局问题
- 缺点: 自定义类型无法判断,可用instanceof
4.1 为什么typeof null===object
这是JavaScript历史遗留的问题。js中的数据在底层是通过二进制来进行存储的,其中前三位代表其数据的类型,若前三位都为0则判定为object类型,而null对应机器码的NULL指针,一般为全0,则判定是否是object类型的时候将null也带入了。
- 000:对象
- 1:整数
- 010:浮点数
- 100:字符串
- 110:布尔值
- null:全0
4.2 instanceof的原理
上文说到:instanceof是通过原型链查找的方式判断构造函数的原型是否出现在这个实例对象的原型链上
实现一个自己的myInstanceof就清晰了
function myInstanceof(left, right) {
let prototype = right.prototype;
while (true) {
if (left === prototype) return true
if (left === null) return false;
left = Object.getPrototypeOf(left);//left.__proto__
}
}
5.遍历对象的方法
- for…in:遍历所有包括原型上的可枚举属性,可利用hasOwnProperty判断属性是否是特定自身非继承的。
- 索引为字符串型数字
- 遍历顺序不一定按照实际的内部顺序
- 遍历所有属性,包括原型
- 适合遍历对象,不要用来遍历数组
- for…of:遍历添加了iterator接口的对象
- 遍历为对象属性值
- 应用数组、类数组、Set、Map
- forEach
- 不能使用break打断循环
- 可以用try catch停止
- Object.keys:返回对象的可枚举属性,不包括symbol属性。
- Object.values:返回对象的可枚举属性值,不包括symbol属性。
- Object.entries:返回对像的可枚举属性的所有键值对组成的数组
- Object.getOwnPropertyNames:返回对象所有自身属性的属性名,包括不可枚举属性但不包括symbol
二、变量
1.变量的声明方式
- 1.var:(ES6之前的声明方式)
- 没有块的概念,可以跨块访问,不能跨函数访问。
- 存在变量提升(声明提到当前作用域最前面,赋值不变)
- 同一变量运行重复声明赋值
- 2.let
- let声明变量在块级内有效
- let其实也存在变量提升,只是在变量显式赋值之前不能对变量进行读写,否则就会报错,也就是暂时性死区
- 不允许重复声明
- 3.const
- const声明常量
- 再块级内有效
- 同let一样存在暂时性死区
- 声明必须赋值
- 声明引用类型只是引用地址值不变,内部内容可以更改
- 4.function
- 声明一个函数
- 不会立即执行,调用才执行
- 5.class
- 声明一个类
- 贴近面向对象写法
- 6.import
- 可用于模块中加载引入变量
- import具有提升效果,是在编译时加载的
- import引入的变量是只读的
2.变量对象的创建与执行
我们经常提到变量提升,函数提升,然后让我们求输出结果,如下
console.log(a);
console.log(b);
var a = 3;
var b = 4;
console.log(a);
function a() {
console.log('a function');
}
console.log(a);
最终输出:
function a(){
console.log('a function')
}
undefined
3
3
其实对整个变量对象执行创建过程理解就可知道答案:
VO:变量对象,AO:活动对象
- 创建阶段
- 检查function函数在VO中声明创建属性,如果有同名属性则覆盖
- 检查变量声明:如果是var声明,则在VO中创建并赋值undefined,如果有同名则不创建(因为怕覆盖同名函数),如果是let、const声明则只会创建属性不会赋值(所以其实也变量提升了,只是不能使用)
- 执行阶段
进入执行阶段,VO转变为AO,此时AO里面的属性都可以进行访问。
现在带着理解再来看上面立例题就是带着规则在脑中执行一遍即可:
创建阶段: 从上往下检查,根据有规则2遇到变量a,声明并赋值,遇到变量b声明并赋值,此时
VO={
a:undefined,
b:undefined
}
接着遇到函数声明a,根据规则1覆盖,则此时
VO={
a:function(){console.log('a function');},
b:undefined
}
执行阶段:
VO变为AO,可以访问了
AO={
a:function(){console.log('a function');},
b:undefined
}
从上往下,
- 第一个console.log(a),查找AO,输出函数a
- 都二个console.log(b),查找AO,输出undefined
- 此时对a进行赋值为3,改变AO
AO={
a:3,
b:undefined
}
- 第三个console.log(a),查找AO,输出值3
- 第四个同理输出值3
三、作用域及作用域链
1.作用域
- 定义:定义为一套规则,用来告诉引擎如何在当前作用域以及嵌套的子作用域中根据变量名或函数名进行变量查找
- 分类:
- 全局作用域
- 函数作用域
- 块级作用域(ES6新增)
全局作用域:即可以在全局调用的变量或函数,有以下几种
- window对象上的属性
- 全局声明的变量或函数
- 直接声明赋值,没有用var、let、const
函数作用域:在函数内部声明的变量或函数,只在函数内有效,函数执行完则销毁
块级作用域:ES6新增概念,通过{}包裹的都算块,如if、for等
2.作用域链
- 定义:理解为在当前执行上下文中进行变量或函数的查找,未找到则通过外部引用去外一级上下文中查找,直到最外层,这样就形成了一条由内而外的查找链,作用域链。
- 组成:
- [[scope]]属性:用来指向父级查找
- AO:变量对象
四、this绑定
- 在全局环境下,this指向的是window对象
1.默认绑定
独立的函数调用即会认为是默认绑定
- 正常模式下指向window
- 严格模式下为undefined
// 'use strict' //undefined
function getThis() {
console.log(this); //window
}
getThis()
2.隐式绑定
通过对象调用的方式调用函数即认为是隐式绑定
- 隐式绑定的函数的this始终指向调用它的对象
// 'use strict' //obj
function getThis() {
console.log(this); //obj
}
let obj = {
get: getThis
}
obj.get()
注意:
- 始终指向最近调用它的对象,就算深层嵌套调用也是指向最近调用。
- 隐式丢失:如果将boj.get方法赋给另一个变量去调用就不属于当前对象的隐式调用了
3.显示绑定
我们人为操作的this指向称为显示绑定,因为它是可见的,主要有call、apply、bind
- call(context,a,b…)
- apply(context,arr)
- bind(context,a,b…)
其中第一个参数都是设置this指向的对象。
区别
- call、bind的参数为参数列表,apply参数为数组。
- call、apply返回执行结果,bind返回一个绑定this后的函数
function getThis(age, sex) {
console.log(this.name + age + sex);//xy21男
};
let obj = {
name: 'xy',
}
getThis.call(obj, 21, '男');
4.new绑定
构造函数的this指向它的实例对象
function Person() {
this.name = 'xy'
}
let person = new Person()
console.log(person.name);//xy
这与new的实现有关
- 创建一个空对象
- 将空对象的__proto__指向构造函数的原型
- 将构造函数的this指向空对象
- 如果构造函数没有返回其他对象,则返回此对象
打算后续在手写系列中添加,这里暂不添加代码。
5.内置函数绑定
- setTimeout、setInterval默认指向window
setTimeout(() => {
console.log(this); //window
}, 0)
setInterval(() => {
console.log(this); //window
}, 0);
- 数组方法,forEach、map、filter等,以forEach为例,forEach(function,thisArg),这里直接调用默认指向window,可通过第二个参数改变
let arr = [1, 2, 3]
let obj = {
a: 3
}
arr.forEach(function (item) {
console.log(this); //window
})
arr.forEach(function (item) {
console.log(this); //{a:3}
}, obj)
6.事件调用
像点击事件等,绑定在谁身上this就指向谁
let btn = document.querySelector('button');
btn.addEventListener('click', function () {
console.log(this);//<button>点击按钮</button>
})
7.箭头函数
箭头函数不绑定this,其this为当前函数所在作用域下的this
优先级
new绑定>bind显示绑定>隐式绑定>默认绑定
四、闭包
由于作用域的向上查找机制,符合一定条件下就会产生闭包~
- 定义: 指有权访问其他函数作用域下属性变量的函数
- 产生: 一个父级函数内部声明了一个子函数,子函数内部引用了父函数的属性变量
- 缺点: 使用全局变量的闭包不再使用的话就会产生内存泄漏
- 作用
- 创建私有变量
function Num(value) {
return {
get() {
return value;
}
}
}
const num = Num(123)
console.log(num.get()); //123
这里只能通过num.get()方法进行获取。
- 模拟块级作用域
在var声明的for循环内,通过立即执行函数的形式保留每次传入异步对应的i值,这是因为闭包的引用。
for (var i = 0; i < 5; i++) {
(
function (i) {
setTimeout(() => {
console.log(i);
}, 1000)
}
)(i)
}
应用场景
- 防抖节流,用来确保在一定时间内的高频操作都是对同一个计时器的操作。
- 单例模式,通过将返回引用实例的函数,使实例一直存活,并进行判断,有则返回无则创建。
打算后续在手写系列中添加,这里暂不添加代码。
五、原型与原型链
1. 原型
- 分为显示原型(prototype)和隐式原型(
__proto__
) - 显示原型可用于共享属性方法
- 隐式原型可用于指向构造函数的显示原型
1.1 原型、构造函数、实例的关系
从图中可知
- 构造函数内部有对应的显示原型属性prototype
- 显示原型内部有对应的constructor指向构造函数
- 构造函数创建的实例对象内部有隐式原型
__proto__
指向构造函数的显示原型
1.2 原型相关的方法
- Object.create(prototype):创建一个原型是prototype的实例对象
- Object.getPrototypeOf(obj) :返回obj对象的显示原型
- Object.setPrototypeOf(obj1,obj2):用于设置obj1的显示原型为obj2
2.原型链
首先记住:每一个对象内部都有隐式原型(__proto__
)属性
由于实例对象内部的隐式原型指向了它构造函数的显示原型prototype,而显示原型也是对象它内部也有__proto__
指向了上一级的prototype,这样一系列由__proto__和prototype组成的链称为原型链。
2.1 原型链相关方法
- Obj1.prototype.isProtitypeOf(obj2):用于检查obj1是否在obj2的原型链上
六、赋值、浅拷贝、深拷贝
1.赋值
赋值后的变量和原来的其实是指同一个变量,只是多了一个标识名的感觉,就如番茄和西红柿指的其实是一个东西
let person1 = {
name: 'xy',
age: 21,
hobbies: ['sing', 'study']
}
let person2 = person1;
person2.name = 'xx',
person2.hobbies[1] = 'dance'
console.log(person1);
console.log(person2);
这里令person2=person1,然后修改person2的name属性和hobbies属性,然后输出person1、person2发现修改后一样
2.浅拷贝
浅拷贝是创建了一个新的空间,然后遍历原来的对象,如果是基本类型,直接拷贝,如果是引用类型,拷贝其引用地址,所以其实对待引用类型,新旧对象指向的是同一块内容,会互相影响。
let person1 = {
name: 'xy',
age: 21,
hobbies: ['sing', 'study']
}
let person2 = Object.assign({}, person1);
person2.name = 'xx',
person2.hobbies[1] = 'dance'
console.log(person1);
console.log(person2);
可见这里改变person2,而person1的name没改变而hobbies发生改变
- 浅拷贝的方式
2.1 Object.assign()
Object.assign(obj1,obj2)作为合并对象的方法,它是将obj1、obj2合并然后返回一个新对象,如果我们将obj1为空对象就可以模拟拷贝的效果,它是属于浅拷贝。如上述例子
2.2 解构赋值
let person1 = {
name: 'xy',
age: 21,
hobbies: ['sing', 'study']
}
let person2 = {...person1};//对象解构
console.log(person2);
person2.name = 'xx',
person2.hobbies[1] = 'dance'
console.log(person1);
console.log(person2);
3.深拷贝
深拷贝也是创建一个新的空间,然后遍历原对象,基本类型就直接拷贝,引用类型拷贝其内容数据存放在自己新开辟的堆空间中,其引用地址不一样,是两个独立的存放空间,不会相互影响。
let person1 = {
name: 'xy',
age: 21,
hobbies: ['sing', 'study']
}
let person2 = JSON.parse(JSON.stringify(person1));
person2.name = 'xx',
person2.hobbies[1] = 'dance'
console.log(person1);
console.log(person2);
由于基本类型引用类型都是独立的空间,因此互不影响
- 深拷贝的方式
3.1 JSON.parse(JSON.stringify(obj)
存在的问题
- 不能正确处理正则表达式(RegExp),会变成空对象
- 不能处理时间对象(Date),会返回时间字符串
- 如果有值为function、undefined、symbol,则不会复制
- 如果有NaN、Infinity值,会变成null
- 如果有实例对象成为值,则无法得到其constructor
3.2 递归
通过递归简单实现,不包含对各种对象类型的边界处理。
function deepClone(obj) {
//判断是数组还是对象
const target = Array.isArray(obj) ? [] : {};
for (let i in obj) {
//基本类型处理
if (typeof obj[i] != 'object' || obj[i] === null) {
target[i] = obj[i]
} else {
//引用类型处理
target[i] = deepClone(obj[i])
}
}
return target
}
三者区别总结
用一张表可概括
拷贝方式 \ 改变对象的属性类型 | 基本类型 | 引用类型 |
---|---|---|
赋值 | 改变 | 改变 |
浅拷贝 | 不变 | 改变 |
深拷贝 | 不变 | 不变 |