Javascript八股文
基础问题
Javascript 中有什么数据类型?他们的区别的有什么?
数据类型:基本类型和引用类型
基本类型:Number、String、Boolean、Null、Undefined、Symbol、BigInt(ES6 新增)
引用类型:Object、Array、Function、Date等
区别:
声明变量时不同的内存地址分配:简单类型的值存放在栈中,在栈中存放的是对应的值;引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
不同的类型数据导致赋值变量时的不同:简单类型赋值,是生成相同的值,两个对象对应不同的地址;复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象谈谈 Javascript 中的类型转换机制
类型:自动转换(隐式转换)和强制转换(显示转换)
谈谈 Javascript 中的闭包,闭包有什么特点?
定义:闭包就是一个函数和对其周围状态的引用的组合。
1
2
3
4
5
6function funcOutside() {
let a = 1;
function funcInside() {
console.log(a); //内部函数调用外部函数的变量,由于闭包特性是可以访问到的
}
}特点: 创建私有变量;延长变量的生命周期
操作符’==’和’===’的区别
==操作符,即为等于操作符,在变量进行比较的时候会先进行隐式转换,然后再进行变量之间的比对。
1
2
3let num1 = 1;
let str1 = "1";
console.log(num1 == str1); // true===操作符,即为全等操作符,在变量进行比较的时候会直接进行变量之间的比对,不会如等于操作符一样先进行隐式转换。
1
2
3let num1 = 1;
let str1 = "1";
console.log(num1 === str1); // false深拷贝和浅拷贝的区别
浅拷贝
浅拷贝:创建新的数据,这份新的数据拥有原始数据属性值的一份精准拷贝。如果是基本类型拷贝,拷贝的是属性的值;如果是引用类型拷贝,拷贝的是内存地址。即拷贝的数据和原始数据共享同一内存地址。
实现一个浅拷贝
1
2
3
4
5
6
7
8
9function copy(obj) {
const newObj = {}; // const不可以修改内存地址,但可以修改属性
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
newObj[prop] = obj[prop];
}
}
return newObj;
}在 Javascript 中存在浅拷贝现象的方法还有:
- Object.assign()
- Array.prototype.slice(), Array.prototype.concat()
- …
深拷贝
深拷贝:开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
实现一个深拷贝
1
2
3function(){
....
}在 Javascript 中存在深拷贝现象的方法还有:
- _.cloneDeep()
- jQuery.extend()
- JSON.stringify()
- …
什么是作用域链?
作用域
作用域,即为变量和函数生效的区域或集合。
作用域一般分为:全局作用域、函数作用域和块级作用域(ES6+)全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。
1
2
3
4
5let num1 = 1; // 全局变量
function func() {
console.log(num1);
}
func(); // 输出 1函数作用域
函数作用域另称局部作用域,当一个变量是声明在函数内部时,它只能被函数内部进行访问,不能由函数外部进行访问。
1
2
3
4
5
6function func() {
let num1 = 1;
console.log(num1); //输出 1
}
func();
console.log(num1); //Undefined块级作用域
块级作用域是由 ES6 中新增的 let 和 const 关键字,在一对大括号中声明的 let 和 const 关键字存在于块级作用域中,在大括号之外不可访问。
1
2
3
4
5
6{
let num1 = 1;
const num2 = 2;
console.log(num1, num2); // 1, 2
}
console.log(num1, num2); // Undefined, Undefined词法作用域
词法作用域就是静态作用域,一个变量一旦被创建就会被定义好其静态作用域,而不是代码执行过程中定义好的。
作用域链
作用域链就是当 Javascript 去使用一个变量的时候,会从当前作用域下开始寻找,如果没有找到则会依次向上寻找,直至最高层全局作用域。如果没有找到,则会隐式创建变量或者报错。
1
2
3
4
5
6
7
8
9
10
11
12let num1 = 1;
function func1() {
let num2 = 2;
function func2() {
let num3 = 3;
console.log(num3); // 当前作用域
console.log(num2); // 往上找到num2,闭包
console.log(num1); // 在全局作用域找到num1
}
console.log(num3); // 不可往下找,报错Undefined
}
func1();谈谈 Javascript 中的原型和原型链
原型
Javascript 是一种基于原型的语言,每个对象都会有对应的原型。对象的属性和方法定义在 Object 的构造器函数的 prototype 属性上。
原型对象
1
2
3
4
5
6
7
8
9
10
11
12{
constructor: ƒ 'YourFunction'(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}原型链
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
总结
- 一切对象都继承自 Object 对象,Object 对象直接继承根源对象 Null。
- 一切函数对象都继承自 Function 对象。
- Object 直接继承自 Function 对象。
- Function 的proto指向自己的原型对象,最终指向 Object 对象。
在 Javascript 中如何实现继承?
常见的继承方式:extends 关键字(ES6+)、原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。
原型链继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。
1
2
3
4
5
6
7
8
9function Parent() {
this.name = "parent1";
this.play = [1, 2, 3];
}
function Child() {
this.type = "child2";
}
Child1.prototype = new Parent();
console.log(new Child());构造函数继承
父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法。
1
2
3
4
5
6
7
8
9
10
11
12
13function Parent() {
this.name = "parent1";
}
Parent.prototype.getName = function () {
return this.name;
};
function Child(){
Parent1.call(this)
this.type = 'child'
}
let child = new Child()
console.log(child) // 正常运行
console.log(chile.getName()) // 报错组合继承
组合继承则将前两种方式继承起来。
原型式继承
借助Object.create方法实现普通对象的继承。
寄生式继承
寄生式继承在原型式继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法。
寄生组合式继承
借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式。
谈谈你对this对象的理解
this关键字是什么
this关键字是函数运行的时候自动生成的一个内部对象,只能在函数内部使用,this总指向调用他的对象。并且,this关键字一旦确定,不可更改。
绑定规则和优先级
- 默认绑定
- 隐式绑定
- 显示绑定
- new绑定
new绑定>显示绑定>隐式绑定>默认绑定
箭头函数
与传统函数不同,箭头函数中的this关键字指向该箭头函数被定义的对象。
Javascript中执行上下文和执行栈是什么?
执行上下文
执行上下文是一种对Javascript的代码执行环境的一种抽象概念,即只要有Javascript代码则会有执行上下文。
执行上下文的类别
- 全局执行上下文:变量在全局内,只有一个全局执行上下文,浏览器的全局对象是window。
- 函数执行上下文:可以有无数个,当函数被调用执行的时候,就会创建一个对应的函数执行上下文。
- Eval函数执行上下文:运行在eval函数中的代码,使用频率很低。
执行上下文的生命周期:
graph LR 创建阶段-->执行阶段-->回收阶段
执行栈
执行栈又称调用栈,它拥有后进先出的结构,用于存储所有的执行上下文。
当Javascript引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中,后续每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中,引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文。谈谈typeof和instanceof的区别
typeof
- typeof返回的是一个变量的基本类型。
- typeof可以返回除(Null)外的基本类型,除(Function)外的引用类型。
instanceof
- instanceof返回的是布尔值。
- instanceof可以准确判断变量的引用类型,但不可以判断变量的基本类型。
说说new操作符具体做了什么?
- 创建一个新的对象。
- 把新的对象的_proto_指向原函数的构造函数。
- 将构造函数内的this指向到新对象上。
- 检查构造函数返回值。
new操作符具体流程:
graph LR 新建对象-->_proto_绑定-->this绑定-->检查返回值
说说Javascript中的事件模型
事件
在HTML文档或浏览器中发生的一系列交互操作,就称为事件。
事件流
由于DOM是一个树结构,因此在出现父子节点绑定事件,触发子节点的时候会引起顺序问题,由此有事件流之概念。
事件流有三个阶段:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
事件模型
事件模型分为三种:
- 原始事件模型
- 标准事件模型
- IE事件模型
原始事件模型
没有事件冒泡阶段
1
<button onclick="func()"> //此处的onclick绑定事件就是原始事件绑定
标准事件模型
1
2
3
4//绑定事件
addEventListener(eventType, handler, useCapture)
//删除事件
removeEventListener(eventType, handler, useCapture)IE事件模型
没有事件冒泡阶段
1
2
3
4//绑定事件
attachEvent(eventType, handler)
//删除事件
detachEvent(eventType, handler)谈谈什么是事件代理?
事件代理就是指把事件委托在目标元素的外层元素上,当触发目标元素时,通过事件冒泡机制触发到它的外层绑定元素上,执行相应函数。
整体有两大优点:
- 减少内存使用。
- 动态绑定,避开重复工作。
ajax原理是什么?
- 创建核心XmlHttpRequest(Xhr)对象。
- 通过Xhr对象的open方法,与服务器连接。
- 构造请求所需要的数据内容,通过Xhr的send方法发送给服务端。
- 通过Xhr对象提供的onreadystagechange监听服务端变化。
- 接收并处理服务端返回的结果数据。
- 将处理结果用javascript操作DOM实现更新。
graph LR 创建Xhr对象-->|open方法|连接服务器-->|send方法|发送请求-->|onreadystagechange|监听变化-->接收并处理结果-->|操作DOM|更新页面
谈谈bind,apply和call三者的区别
共同点
- 目的都是修改this关键字的指向。
- 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。
不同点
- apply和call是单次传参,bind可多次传参。
- apply的参数为数组,call的参数为参数列表.
- bind返回绑定this参数后的函数,apply和call立即执行函数。
说说你对事件循环的理解
同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就是事件循环。
任务分为:
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行。
- 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等
- 微任务:一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
- 宏任务:宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。
说说Javascript中常见的内存泄露情况
- 意外的全局变量
1
2
3
4
5
6
7function func1(){
wrong = 1 // 变量没有在任何作用域有定义,会在全局作用域中隐式定义
}
function func2(){
this.wrong = 2
}
func2() //此时函数内this指向window,又会隐式生成一个全局变量 - 闭包的私有变量
1
2
3
4
5
6
7function funcOutside(){
const obj = {......}
function funcInside(){
console.log(obj) // 没有清理对私有变量的引用
obj = null // 需要这样正确清理
}
} - 未正确释放的DOM引用
1
2
3
4const refA = document.getElementById('refA')
console.log(refA)
//需要清理
refA = null - 计时器中的引用
1
2
3
4
5
6
7
8var someResource = getData();
setInteral(function(){
let node = document.getElementById('node')
if(node){
//外部引用
node.innerHTML = JSON.stringify(someResource)
}
}, 1000)
- 意外的全局变量
谈谈Javascript中的几种本地存储
- cookie
- sessionStorage
- localStorage
不同点
- 存储大小:cookie只能存储最多4K大小的文本数据,而后两者可以存储5MB以上的数据。
- 生命周期:cookie的生命周期取决于设置的过期时间,sessionStorage是在当前窗口关闭的时候会自动删除,localStorage存储持久数据,关闭浏览器不会丢失数据。
- 交互方法:cookie的数据会自动传输给服务器,服务器也可以写cookie给客户端。localStorage和sessionStorage只能在本地存储。
谈谈Javascript中数字精度丢失的问题
简单的说,就是十进制数字需要先转为二进制数,然后再进行计算后转回十进制数得到结果。其中,进制的转换会由于位数限制、某些数转二进制出现无限循环造成数值转换后会出现偏差,导致结果不符合直觉。
什么是防抖和节流?
防抖
防抖就是指在n秒后再执行事件,如果在n秒内重复触发,则重新计时。
节流
节流就是指在n秒内只执行一次事件,如果在n秒内重复触发,只有一次生效。