文章目录
- 1 如何通过JS给元素添加class
- 2 如何阻止事件冒泡以及默认行为
- 3 Event Loop(事件轮询)理解
- 4 JavaScript原始数据类型
- 5 深拷贝浅拷贝
- 6 `typeof`与`instanceof`之间的区别
- 7 this指向问题
- 8 JS中为什么0.1+0.2!== 0.3?(浮点数误差)
- 9 GET与POST的区别
- 10 什么是闭包?
- 11 `==`和`===`的不同
- 12 javascript的同源策略
- 13 JS对象
- 14 JS 常见的 5 种继承方式
- 15 V8的垃圾回收机制
- 16 设计模式
- 17 什么是防抖和节流
- 实现深度优先遍历和广度优先遍历
- JS改变数组的方法有哪些
- JS判断数组的方法
- 什么是类数组对象?
1 如何通过JS给元素添加class
let obj = document.getElementsById('obj');
// 添加class属性值
// 方式一
obj.className += 'new active';
// 方式二:属性值不能有空格,例如'new active'
obj.classList.add('active');
// 移除div的class属性
obj.classList.remove('active');
2 如何阻止事件冒泡以及默认行为
事件冒泡
- 触发某类事件,会向这个对象的父级对象传播,直至它被处理,或者它到达了对象层次的最顶层(
windows
)
默认行为
- 浏览器的一些默认的行为。
- 例如:点击超链接跳转,点击右键会弹出菜单,滑动滚轮控制滚动条等
事件冒泡有什么作用
- 事件冒泡允许多个操作被集中处理(把事件处理器添加到一个父级元素上,避免把事件处理器添加到多个子级元素上)
- 让你在对象层的不同级别捕获事件。
阻止事件冒泡
DOM
中提供stopPropagation()
方法,但IE不支持,使用event对象在事件函数中调用就行。IE
中提供的是,cancelBubble
属性,默认为false,当它设置为true时,就是阻止事件冒
function stopBubble(e) {
if(e && e.stopPropagation){
e.stopPropagation();
} else {
window.event.cancelBubble = true;
}
};
阻止默认行为
- DOM中提供
preventDefault()
方法来取消事件默认行为 - IE中提供的是
returnValue
属性,默认为true,当它设置为false时,就是取消事件默认行为 - jQuery中提供了
preventDefault()
方法来阻止元素的默认行为
function stopDefault(e){
if(e && e.preventDefault) {
e.preventDefault();
} else {
window.event.returnValue = false;
}
return false;
};
3 Event Loop(事件轮询)理解
-
Event Loop
是javascript的执行机制,用于等待
和发送消息
的事件,是实现异步
的具体解决方案 -
在程序中设置两个线程:一个负责程序本身的运行,称为"
主线程
";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"EventLoop线程"(可以译为"异步线程
")
异步任务
ES6标准中,异步任务又分为两种类型,宏任务
(macrotask)和微任务
(microtask)。
-
宏任务:由宿主环境提供,比如:
setTimeout
、setInterval
、网络请求
、用户I/O、script(整体代码)
、UI rendering、setImmediate(node)。 -
微任务:语言标准(ECMAScript)提供,如:
process.nextTick
(node环境中,要先于其他微任务执行)、Promise
、Object.observe、MutationObserver。
任务运行的流程
- 同步代码(主线程)直接执行;
- 异步函数先放在异步队列中。任务队列的读取顺序是先读取所有微观任务队列执行后再读取一个宏观任务队列,再读取所有微观任务队列,再读取一个宏观任务队列…)
- 待同步函数执行完毕,轮询执行 异步队列中的函数;
- 以上步骤不断重复执行,就形成了事件轮询;
setTimeout(fn, 0) 的作用
调用 setTimeout
函数会在一个时间
过去后在队列
中添加一个任务 。
当前代码(执行栈)执行完,队列中没有其它任务,主线程才会去执行它指定的回调函数
因此第二个参数仅仅表示最少的时间,而非确切的时间。
4 JavaScript原始数据类型
JavaScript
有 8 种原始数据类型:
简单数据类型:String、Number、Boolean、Null、Undefined、Symbol、BigInt。
复杂数据类型(引用数据类型):Object。
基本类型和引用类型的区别
不同的存储方式:
- 基本类型:
基本类型
值在内存中占据固定大小
,保存在栈
内存中。 - 引用类型:
引用类型
的值是对象
,保存在堆
内存中,而栈内存存储的是对象的变量标识符
和对象在堆内存的存储地址
。
不同的复制方式:
- 基本类型:从一个变量向另一个变量复制基本类型的值,会
创建
这个值的一个副本,并将该副本复制
给新变量。 - 引用类型:从一个变量向另一个变量复制引用类型的值,
复制
的是指针
,最终两个变量都指向同一个对象
。
underfined与null的区别
undefined
表示一个变量自然的、最原始
的状态值null
则表示一个变量被人为的设置为空对象
,而不是原始状态。
Symbol
Symbol
表示唯一
的标识符
。- 对象的属性键只能是
字符串类型
或者Symbol
类型。用来表示对象属性的唯一性
Symbol
保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同Symbol
不会被自动转换为字符串
// id 是 symbol 的一个实例化对象
let id = Symbol();
// id 是描述为 "id" 的 Symbol
let id = Symbol("id");
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
5 深拷贝浅拷贝
- 浅拷贝:对
引用类型
仅复制了引用
,彼此之间的操作会互相影响 - 深拷贝:在
堆
中重新分配内存
,不同的地址,相同的值,互不影响
深拷贝:JSON.parse(JSON.stringify(Object))
JSON.stringify()
:把一个js对象序列化为一个JSON字符串JSON.parse()
:把JSON字符串反序列化为一个js对象
let copyObj = JSON.parse(JSON.stringify(obj));
const kitchenSink = {
set: new Set([1, 3, 3]),
map: new Map([[1, 2]]),
regex: /foo/,
deep: { array: [ new File(someBlobData, 'file.txt') ] },
error: new Error('Hello!')
}
const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))
// 会得到
{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}
JSON.stringify
只能处理基本对象、数组和原子类型。任何其他类型都可以以难以预测的方式处理。例如,日期被转换为字符串。而 Set 只是转换为 {} 。
深拷贝:Lodash 的 _.cloneDeep
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
// ✅ All good!
const clonedEvent = cloneDeep(calendarEvent)
深拷贝:structuredClone
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
// ✅ All good!
const clonedEvent = cloneDeep(calendarEvent)
浅拷贝:Object.assign()
语法:
Object.assign(target, ...sources);
返回值:target
对象
- 第一个参数是拷贝的
目标target
,剩下的参数是拷贝的源对象sources
(可以是多个) - 在对象和数组的第一层级是深拷贝,其后都是浅拷贝。
a = {b:{c:123}}
d={}
Object.assign(d,a)
d==a // false
d.a==a.a // true
浅拷贝:扩展运算符
...
扩展运算符也是ES6
中的新特性,它可以方便的浅复制一个对象。- 在对象和数组的第一层级是深拷贝,其后都是浅拷贝。
a = {b:{c:123}}
d={}
d={...a}
d==a // false
d.a==a.a // true
6 typeof
与instanceof
之间的区别
使用 typeof
操作符来检测变量的数据类型。不能检测Null、Array、Object
typeof "John" // 返回 string
typeof 3.14 // 返回 number
typeof false // 返回 boolean
typeof [1,2,3,4] // 返回 object
typeof {name:'John', age:34} // 返回 object
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。(常用来判断某个实例对象的引用类型,不能用来判断基础类型)
var a=new Array();
alert(a instanceof Array); // true
7 this指向问题
this
是执行上下文
中创建是确定的一个在执行过程中不可更改的变量
- 全局环境中
this
都是指向顶层对象
(浏览器中是window
)。 - 当函数独立调用的时候,在
严格模式
下它的this
指向undefined
,在非严格模式
下,当this
指向undefined
的时候,自动指向全局对象
(window)
箭头函数的this是什么?
- 箭头函数的
this
始终指向函数定义时的this
,而非执行时。 - 箭头函数中没有this绑定,必须通过查找作用域链来决定其值。
- 如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象。
怎么改变 this 的指向
改变 this
的指向我总结有以下几种方法:
- 使用
ES6
的箭头函数
- 在函数内部使用
that = this
- 使用
apply
、call
、bind
new
实例化一个对象
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100);
}
};
a.func2() // this.func1 is not a function
最后调用 setTimeout
的对象是 window
,但是在 window
中并没有 func1
函数。
箭头函数进行修改
func2: function () {
setTimeout( () => {
this.func1()
},100);
}
在函数内部使用 _this = this
先将调用这个函数的this
保存在变量 _this
中,这样 _this
就不会改变了。
func2: function () {
var _this = this;
setTimeout( function() {
_this.func1()
},100);
}
使用 apply、call、bind
使用 apply
、call
、bind
函数也是可以改变 this
的指向的
func2: function () {
setTimeout( function () {
this.func1()
}.apply(a),100);
}
call apply 与 bind
call 、apply的区别
foo.call(this, arg1,arg2,arg3) == foo.apply(this, [arg1, arg2, arg3])
call
, apply
都属于Function.prototype
的一个方法,作用就是借用别人的方法来调用自己
相同点:两个方法产生的作用是完全一样的
不同点:方法传递的参数不同,call支持多个参数,apply要将多个参数转化成数组
8 JS中为什么0.1+0.2!== 0.3?(浮点数误差)
JavaScript
中所有数字包括整数和小数都只有一种类型 — Number
。使用 64
位固定长度来表示,也就是标准的 double
双精度浮点数。
由于0.1
转换成二进制时是无限循环的,所以在计算机中0.1
只能存储成一个近似值
解决方法:parseFloat((0.1 + 0.2).toFixed(10))
9 GET与POST的区别
GET
请求通过URL地址
发送请求参数,参数可以直接在地址栏中显示,安全性较差
;POST
是通过请求体
发送请求参数,参数不能直接显示,相对安全
;GET
请求URL地址长度限制在255字节
内,POST
请求没有长度限制
;
10 什么是闭包?
简单来说就是函数嵌套函数,内部函数可以引用来外部函数的变量,因为外部函数变量一直在被引用,所以不会被释放
缺点:闭包会产生内存泄漏
怎么解决:优化内存泄漏,在使用完闭包里的数据后,将数据置为null
。
function demo(){
let i = 1;
return function sum(){
i++;
console.log(i);
}
}
11 ==
和===
的不同
相等运算符 “==” 如果两个操作数不是同一类型,那么相等运算符会尝试一些类型转换,然后进行比较。
相等运算符"=="算法(EEA)
- 如果操作数有不同的类型:
如果一个操作数为 null 而另一个 undefined,则它们相等 - 如果一个值是数字,另一个是字符串,先将字符串转换为数字,然后使用转换后的值比较
- 如果一个操作数是布尔值,则将 true 转换为 1,将 false 转换为 0,然后使用转换后的值比较
- 如果一个操作数是一个对象,而另一个操作数是一个数字或字符串,则使用OPCA将该对象转换为原原始值,再使用转换后的值比较
12 javascript的同源策略
一段脚本只能读取来自于同一来源的窗口和文档的属性,这里的同一来源指的是主机名、协议和端口号的组合
13 JS对象
1.普通对象与函数对象
凡是通过 new Function()
创建的对象都是函数对象,其他的都是普通对象。
2.constructor
构造函数
- 构造函数,是一种特殊的方法。主要用来在创建对象时初始化对象。每个构造函数都有
prototype
(原型)属性 - 每个函数都有
constructor
(构造函数)属性,这个属性是一个指针,指向该实例的构造函数,
3.prototype
原型对象
-
每个
函数对象
都有一个prototype
属性,这个属性指向函数的原型对象
。 -
prototype
原型对象是用来给实例共享属性和方法
的。 -
每个
对象
都有__proto__
属性,但只有函数对象
才有prototype
属性 -
原型对象(
Person.prototype
)是 构造函数(Person
)的一个实例。
4.原型链
当实例化对象调用某个方法时会先在自身和原型上查找,然后是在_proto_
上一层层查找,直到查询到结果位置,这种方式就是原型链。
5.__proto__
对象
(对象及函数对象),都有一个__proto__
的内置属性,用于指向创建它的构造函数
的原型对象
。
Person.prototype.constructor == Person;
person1.__proto__ == Person.prototype;
person1.constructor == Person;
14 JS 常见的 5 种继承方式
1 原型链继承
每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。
function Parent1() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child1() {
this.type = 'child2';
}
Child1.prototype = new Parent1();
console.log(new Child1());
缺点:所有实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化
2 构造函数继承(借助 call)
function Parent1(){
this.name = 'parent1';
}
Parent1.prototype.getName = function () {
return this.name;
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
let child = new Child1();
console.log(child); // 没问题
console.log(child.getName()); // 会报错
缺点:只能继承父类的实例属性和方法,不能继承原型属性或者方法
3 组合继承(前两种组合)
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this);
this.type = 'child3';
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
4 原型式继承
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name);
console.log(person4.name === person4.getName());
console.log(person5.name);
console.log(person4.friends);
Object.create
这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承 getName 的方法
5 继承的关键字 extends
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
15 V8的垃圾回收机制
16 设计模式
17 什么是防抖和节流
17.1 防抖(debounce)
函数防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
const _.debounce = (func, wait) => {
let timer;
return () => {
if(timer) clearTimeout(timer);
timer = setTimeout(func, wait);
};
};
应用场景:
连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
17.2 节流(throttle)
限制一个函数在一定时间内只能执行一次。防止页面点击事件触发多次点击
const _.throttle = (func, wait) => {
let timer;
return () => {
if (timer) {
return;
}
timer = setTimeout(() => {
func();
timer = null;
}, wait);
};
};
应用场景:
间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交
区别:防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
实现深度优先遍历和广度优先遍历
JS实现深度优先遍历(DFS)的示例代码如下所示:
function dfs(node) {
if (node === null) return; // 若节点为空则返回
console.log(node); // 输出当前节点值
for (let i = 0; i < node.children.length; i++) {
dfs(node.children[i]); // 对每个子节点进行递归调用
}
}
// 使用示例
const rootNode = { value: 'A', children: [{value: 'B'},{value: 'C'}] };
dfs(rootNode);
以上代码中,dfs()
函数接收一个节点作为参数。首先判断该节点是否为null,若为null则直接返回;然后打印当前节点的值;最后通过for循环遍历当前节点的子节点,并对每个子节点再次调用dfs()
函数,从而实现了深度优先遍历。在这里我们将根节点设置为rootNode
,可以自定义其他节点来测试。
JS实现广度优先遍历(BFS)的示例代码如下所示:
function bfs(node) {
const queue = []; // 创建一个队列存放待访问的节点
let currentLevelNodes = [node]; // 初始化当前层级的节点集合
while (currentLevelNodes.length > 0) {
const nextLevelNodes = []; // 保存下一层级的节点集合
for (let i = 0; i < currentLevelNodes.length; i++) {
const currNode = currentLevelNodes[i];
console.log(currNode); // 输出当前节点值
for (let j = 0; j < currNode.children.length; j++) {
nextLevelNodes.push(currNode.children[j]); // 添加到下一层级的节点集合
}
}
currentLevelNodes = nextLevelNodes; // 更新当前层级的节点集合
}
}
// 使用示例
const rootNode = { value: 'A', children: [{value: 'B'},{value: 'C'}] };
bfs(rootNode);
以上代码中,bfs()
函数同样接收一个节点作为参数。首先创建一个队列queue来存放待访问的节点,并将根节点入队;然后通过while循环不断取出队头元素,并将其标记为已经访问过;之后遍历当前节点的子节点,并将其入队;重复此操作,直到队列为空。在这里我们也将根节点设置为rootNode,可以自定义其他节点来测试。
JS改变数组的方法有哪些
push
、pop
、shift
、unshift
、splice
、sort
、reverse
JS判断数组的方法
Array.isArray()
、instanceof
运算符、Object.prototype.toString.call()
什么是类数组对象?
- 一个拥有
length
属性和若干索引属性的对象就可以被称为类数组对象。 - 类数组对象和数组类似,但是不能调用数组的方法。
- 常见的类数组对象有
arguments
和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。
常见的类数组转换为数组的方法有以下4种:
//(1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
//(2)通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
//(3)通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
//(4)通过 Array.from 方法来实现转换
Array.from(arrayLike);toc]