题库:www.arryblog.com/interview/j…
ES6的新特性
- let 和 const
- 箭头函数
- 模板字符串
const x = 1;
const y = 2;
const result = `${x} + ${y} = ${x + y}`; // result 的值为 "1 + 2 = 3"``
- 对象的解构
`// 使用普通方式获取对象属性`
const person = { name: "Alice", age: 25 };
const name = person.name;``const age = person.age;
// 使用对象解构赋值获取对象属性``const { name, age } = person;
- class和继承
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
class Student extends Person {
constructor(name, age, grade) {
super(name, age);
this.grade = grade;
}
study() {
console.log(`${this.name} is studying in grade ${this.grade}.`);
}
}
symbol
特点
一种永远不会用重复的数据类型。两个symbol类型,用 === 来判断,它们永远不同。
用法
可以添加描述
之前说到,两次创建的Symbol() 一定是不同的Symbol(),我如果想让两次变量指向同一个symbol怎么办? 答案是使用symbol.for();
如果使用for来指定 symbol 的描述,那么可以使用keyFor来获取描述。(如果没用for,那么keyfor也获取不到)。
使用场景
1.纯函数
定义:
- 对外界没有副作用
- 同样的输入,得到同样的输出
例子1:改变了外界的值,因此不是纯函数
var obj2 = {
myname:'111';
}
function test2(obj2){
obj2.myname = 'xiaoMIng';
return obj2;
}
例子2:虽然对外界没有影响,但是每次执行函数,拿到的结果不同,因此不是纯函数。
function test2(obj2){
var newObj = {...obj2};
newObj.myname = 'xiaoMIng' + Math.random();
return newObj;
}
&& 和 || 运算符的用法
&& 如果前面所有都是true,则会返回最后一个值。
|| 返回第一个true。
2.script标签相关
-
async(异步) :当
<script>标签设置了async属性时,浏览器会异步加载外部JavaScript文件,即在加载和解析HTML文档的同时加载脚本。但是,一旦脚本加载完成,浏览器会立即执行它,此时HTML解析可能尚未完成。因此,异步脚本的执行顺序无法保证,可能会在其他普通脚本或defer脚本之前或之后执行。 -
defer(延迟) :当
<script>标签设置了defer属性时,浏览器会在加载和解析HTML文档的同时加载脚本。但与async不同的是,浏览器会等待整个HTML文档解析完成后才执行defer脚本。这意味着defer脚本会按照它们在HTML文档中出现的顺序执行。 -
加载(Loading) :加载是指浏览器从服务器获取HTML文档、CSS样式表、JavaScript脚本等资源的过程。在加载过程中,浏览器会发起HTTP请求以获取资源,然后将资源下载到本地。加载是从服务器获取资源并准备好在浏览器中使用的过程。
-
解析(Parsing) :解析是指浏览器处理HTML文档、CSS样式表、JavaScript脚本等资源的过程。在解析HTML文档时,浏览器会将文档转换为DOM(文档对象模型)结构,以便于JavaScript操作。解析CSS样式表时,浏览器会计算出元素的样式信息。解析JavaScript脚本时,浏览器会执行脚本。解析是浏览器处理资源并准备好在页面中呈现和交互的过程。
需要注意的是,加载css样式不会阻塞html解析,但会阻塞页面渲染。以下例子1,即使css文件特别大,仍会立即console.log,证明加载css样式不会阻塞html解析。<link> 标签预加载图片或js资源时,它通常不会阻塞 HTML 文档的解析或渲染。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>CSS Loading Example</title>
<script>
console.log("Before CSS loading");
</script>
<link rel="stylesheet" href="styles.css" />
<script>
console.log("After CSS loading");
</script>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a simple example to demonstrate CSS loading behavior.</p>
</body>
</html>
另外,link也可以加载js脚本,比如prefetch。但是这样加载的js脚本不会立即执行。
<link>标签在这里只用于预加载JavaScript文件,而不是执行文件。当实际需要这个代码块时,浏览器会再次发起一个请求,但由于之前已经预加载过,所以这次请求会从缓存中加载,从而提高加载速度。
3.特殊的语法,函数
3.1 Object.freeze()
Object.freeze() 是 JavaScript 中用来阻止对象被修改的一个方法。
例子:
const color = {
red: 255,
green: 0,
blue: 0
};
Object.freeze(color);
// 由于对象已冻结,以下操作将无法修改对象
color.red = 100;
color.alpha = 0.5;
delete color.blue;
console.log(color);
// 输出:{ red: 255, green: 0, blue: 0 }
3.2 Object.create()
1.实现继承
const animal = {
species: "Animal",
makeSound: function () {
console.log("Some generic animal sound");
},
};
const dog = Object.create(animal);
dog的__proto__是animal,因此dog有species属性和makeSound()方法。
2.创建一个空对象
const emptyObject = {}; 创建了一个具有 Object.prototype 作为原型的对象。这意味着它会从 Object.prototype 继承属性和方法,如 hasOwnProperty、toString 等。
让我们通过一些示例代码来说明这两种方法创建的对象之间的差异:
javascriptCopy code
const emptyObject1 = Object.create(null);
console.log(emptyObject1); // 输出:{}
console.log(emptyObject1.hasOwnProperty); // 输出:undefined
console.log(emptyObject1.toString); // 输出:undefined
const emptyObject2 = {};
console.log(emptyObject2); // 输出:{}
console.log(emptyObject2.hasOwnProperty); // 输出:[Function: hasOwnProperty]
console.log(emptyObject2.toString); // 输出:[Function: toString]
3.3 slice()
字符串
第一个参数表示开始位置,第二个参数表示结束为止。若开始小于等于结束,则返回空。如果是负数,代表从右往左数。区别在于,是从1开始数,而不是从0。以下是一个例子,起始位置是从右往左数第4个(下标为0),截止位置是从右往左数第二个(下标为3)。遵循左闭右开原则,返回‘12’。
let orginal = '1234'
orginal.slice(-4,-2)//'12'
3.4 requestIdleCallback
每一帧,浏览器空闲的时候触发。此时浏览器渲染已经完成。如果浏览器一直不空闲怎么办?,设置timeout,达到最大等待时间,即使浏览器不空闲,也会触发。requestIdleCallback(doLowPriorityTask, { timeout: 2000 });
3.5 requestAnimationFrame
参考资料:www.cnblogs.com/spoem/p/132…
在下一次重绘之前调用。他不会阻塞本次渲染。(以下例子实现了一个方块不断向下移动的动画)。
追问:requestAnimationFrame 和 setinterval 的区别是什么?
requestAnimationFrame保证在下一帧渲染前调用,但是settinerval不能保证,有其他的JavaScript耗时任务也在宏任务队列中,那么 setinterval 就会等待之前的宏任务执行完,它才会执行。
<!DOCTYPE html>
<html>
<head>
<style>
#box {
position: absolute;
width: 50px;
height: 50px;
background-color: red;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
const box = document.getElementById('box');
let topPosition = 0;
const speed = 1;
function animate() {
// 更新盒子的位置
topPosition += speed;
box.style.top = `${topPosition}px`;
// 使用 requestAnimationFrame 不断调度下一次动画
requestAnimationFrame(animate);
}
// 开始动画
requestAnimationFrame(animate);
</script>
</body>
</html>
4.浅拷贝与深拷贝
4.1 数组
- 浅拷贝:创建一个新数组,将原数组的元素复制到新数组中。如果原数组中有对象或者数组,浅拷贝只会复制对象或数组的引用,而不会复制它们的内容。这意味着,在新数组中修改对象或数组时,原数组中的对象或数组也会受到影响。
浅拷贝示例:
javascriptCopy code
const originalArray = [1, 'hello', { a: 1 }, [4, 5]];
// 使用展开运算符实现浅拷贝
const shallowCopy = [...originalArray];
// 修改新数组中的对象和数组
shallowCopy[2].a = 2;
shallowCopy[3][0] = 100;
console.log(originalArray); // 输出:[1, 'hello', { a: 2 }, [100, 5]]
- 深拷贝:创建一个新数组,并递归地复制原数组的所有元素(包括对象和数组)。深拷贝会创建对象和数组的副本,而不是只复制引用。这意味着,在新数组中修改对象或数组时,原数组中的对象或数组不会受到影响。
深拷贝示例:
function deepCopy(arr) {
const copy = Array.isArray(arr) ? [] : {};
for (let key in arr) {
const value = arr[key];
copy[key] = (typeof value === 'object' && value !== null) ? deepCopy(value) : value;
}
return copy;
}
const originalArray = [1, 'hello', { a: 1 }, [4, 5]];
// 使用自定义 deepCopy 函数实现深拷贝
const deepCopyArray = deepCopy(originalArray);
// 修改新数组中的对象和数组
deepCopyArray[2].a = 2;
deepCopyArray[3][0] = 100;
console.log(originalArray); // 输出:[1, 'hello', { a: 1 }, [4, 5]]
4.2对象
1.对象的浅拷贝
Object.assign():
2.对象的深拷贝(直接忽略了正则表达式和函数)
const originalObject = {
a: 1,
b: 2,
myFunction: function() {
console.log('Hello, World!');
},
};
const copiedObject = JSON.parse(JSON.stringify(originalObject));
console.log(copiedObject); // 输出: { a: 1, b: 2 }
如何遍历对象
for...in会遍历原型对象,object.keys只会遍历自身。
let protyOne = {
b:'我是原型1号上的属性'
}
let protyTwo = {
c:'我是圆形2号上的属性',
}
let obj = {
a:'我是obj上的属性',
}
Object.setPrototypeOf(obj, protyOne);
Object.setPrototypeOf(protyOne, protyTwo);
for (let key in obj) {
console.log('let遍历',`${key}: ${obj[key]}`);
}
// let遍历 a: 我是obj上的属性
// let遍历 b: 我是原型1号上的属性
// let遍历 c: 我是圆形2号上的属性
console.log(Object.keys(obj)) //['a']
foreach和map的区别
1.foreach没有返回值
2.链式调用
由于 map 返回一个新数组,您可以在 map 调用后继续链式调用其他数组方法。而 forEach 返回 undefined,因此无法进行链式调用。
5.与css相关的api
5.1 getBoundingClientRect()
获取当前元素与视口的距离:
const element = document.querySelector('#myElement');
const rect = element.getBoundingClientRect();
console.log(rect.top); // 元素距离视口顶部的距离
console.log(rect.right); // 元素距离视口右侧的距离
console.log(rect.bottom); // 元素距离视口底部的距离
console.log(rect.left); // 元素距离视口左侧的距离
console.log(rect.width); // 元素的宽度
console.log(rect.height); // 元素的高度
获取视口的宽度和高度:
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
//这种方法不包含滚动条
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
获取当前滚动的距离(视口距离文档顶部的距离):
1.window.scrollY 或 window.pageYOffset
2.document.documentElement.scrollTop
场景题目,当元素滚动到屏幕上半部分,自动滚动到顶部:
import React, { useState, useEffect } from "react";
const AutoScrollDiv = () => {
const [scrollPosition, setScrollPosition] = useState(0);
const handleScroll = () => {
const scrollTop = window.scrollY;
const windowHeight = window.innerHeight;
const div = document.getElementById("scrollDiv");
const divRect = div.getBoundingClientRect();
if (divRect.top < windowHeight / 2) {
console.log("进入if");
div.scrollIntoView(true);//true表示滚动顶部 false表示到底部
}
setScrollPosition(scrollTop);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [scrollPosition]);
return (
<div
id="scrollDiv"
style={{
minHeight: "200px",
width: "100%",
backgroundColor: "lightblue",
padding: "10px",
}}
>
<h3>Auto-scrolling div</h3>
<p>当用户将此div滚动到屏幕上半部分时,它将自动滚动到屏幕顶部。</p>
</div>
);
};
export default AutoScrollDiv;
5.2 scrollTo和scrollIntoView
scrollIntoView在上面已经讲到。这里说说scrollTo。scrollTo只对内部有滚动条的元素有用。
以下代码滚动视口,视口的顶部与浏览器顶部距离200px。
setTimeout(() => {
window.scrollTo({
top: 200,
behavior: "smooth",
});
}, 2000);
因此,滚动到顶部的另一种方法:
const scrollToTop = () => {
const div = document.getElementById('scrollDiv');
const divRect = div.getBoundingClientRect();
const scrollOffset = window.pageYOffset || document.documentElement.scrollTop;
const divTopPosition = divRect.top + scrollOffset;
window.scrollTo({ top: divTopPosition, behavior: 'smooth' });
5.3 scrollTop
以下代码是等价的。
setTimeout(() => {
window.scrollTo({
top: 200,
behavior: "smooth",
});
}, 2000);
setTimeout(() => {
document.documentElement.scrollTop = 200;
}, 2000);
6. window对象和document对象
6.1 window
window 对象是浏览器中的一个全局对象
window 对象通常在浏览器环境中使用,用于执行与浏览器窗口和文档内容相关的操作。以下是一些常见且简单的应用场景:
- 弹出对话框:
window.alert("这是一个警告框!");
- 确认框:
if (window.confirm("你确定要继续吗?")) {
console.log("用户点击了确定");
} else {
console.log("用户点击了取消");
}
- 计时器:
// 执行一次性延时操作
window.setTimeout(function () {
console.log("3秒后输出这条消息");
}, 3000);
// 每隔1秒钟执行一次操作
window.setInterval(function () {
console.log("每隔1秒输出一次这条消息");
}, 1000);
- 操作浏览器历史记录:
// 后退一页
window.history.back();
// 前进一页
window.history.forward();
// 跳转到指定历史记录
window.history.go(-2); // 后退两页
- 跳转到其他页面:
window.location.href = "https://www.example.com";
- 获取窗口尺寸:
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
console.log("窗口宽度:" + windowWidth + ",窗口高度:" + windowHeight);
请注意,在许多情况下,可以省略 window 关键字,因为它是 JavaScript 中的全局对象。例如,可以直接使用 alert()、setTimeout() 等方法,而不需要显式地使用 window 对象。
6.2 document
document 对象是浏览器中 JavaScript 的一个全局对象
document 对象提供了许多方法和属性,用于对 HTML 文档进行操作。例如:
- 查找页面中的元素:
getElementById()、getElementsByClassName()、getElementsByTagName()、querySelector()等方法。 - 创建新的元素:
createElement()、createTextNode()等方法。 - 修改元素内容:
innerHTML、textContent等属性。 - 修改元素样式:
style属性。
<!DOCTYPE html>
<html>
<head>
<title>Change Style Example</title>
<script>
function changeStyle() {
var myElement = document.getElementById("example");
myElement.style.color = "red";
myElement.style.fontSize = "24px";
myElement.style.backgroundColor = "yellow";
}
</script>
</head>
<body>
<p id="example">这是一个示例文本。</p>
<button onclick="changeStyle()">点击修改样式</button>
</body>
</html>
- 处理事件:
addEventListener()、removeEventListener()等方法。
7.如何判断是不是数组、函数、对象
export const isNumber = (v: any): v is number => {
return typeof v === 'number';
};
export const isString = (v: any): v is string => {
return typeof v === 'string';
};
export const isNull = (v: any): v is null => {
return typeof v === 'object' && !v;
};
export const isUndefined = (v: any): v is undefined => {
return typeof v === 'undefined';
};
export const isBoolean = (v: any): v is boolean => {
return typeof v === 'boolean';
};
export const isFunction = (v: any): v is Function => {
return typeof v === 'function';
};
export const isObject = (v: any): v is Record<string, any> => {
return !Array.isArray(v) && typeof v === 'object' && v;
};
export const isArray = (v: any): v is any[] => {
return Array.isArray(v);
};
8.左移和右移
console.log(16 >> 2); // 输出:4,因为 16(二进制:10000)向右移动2位后变为4(二进制:100)
console.log(4<<1); //输出8.先把4转化为2进制,100.左移1为,变成1000,转为10进制,就是8。
8.1负数的二进制如何表示
-1的二进制表示过程如下:
1. 首先,找到1的二进制表示。对于32位整数,1的二进制表示为:`00000000 00000000 00000000 00000001`。
1. 然后,找到其按位取反的值,即求其补码。对于上面的二进制表示,按位取反后得到:`11111111 11111111 11111111 11111110`。
1. 最后,将取反后的值加1。在这种情况下,加1后得到:`11111111 11111111 11111111 11111111`。
因此,-1在32位二进制补码表示法中表示为:`11111111 11111111 11111111 11111111`。
8.2 带符号的右移
>>(带符号右移):它将第一个操作数的二进制表示向右移动第二个操作数指定的位数。向右移动时,从左边插入与最左边的位相同的位(也就是说,如果最左边的位是1,那么就在左边插入1,如果是0,就插入0)
9.commonJS和ES6(JavaScript模块系统)
9.1 commonJS
通过require导入的副本。换而言之,即使原始变量被修改了,require得到的变量也不会改变。
//counter.js
let count = 0;
function increase() {
count++;
}
module.exports = {
count,
increase
};
//main.js
const { count, increase } = require('./counter.js');
console.log(count); // 输出 0
increase();
console.log(count); // 输出 0,而不是 1
9.2 ES6
原始模块中的变量改变了,通过import导入的变量也会改变,因为它导入的是一个实时引用
//counter.js
export let count = 0;
export function increase() {
count++;
}
//main.js
import { count, increase } from './counter.js';
console.log(count); // 输出 0
increase();
console.log(count); // 输出 1
9.3 UMU
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD环境
define([], factory);
} else if (typeof exports === 'object') {
// CommonJS环境
module.exports = factory();
} else {
// 浏览器全局变量
root.myModule = factory();
}
}(this, function () {
// 模块定义
function add(a, b) {
return a + b;
}
return {
add: add
};
}));
10. 文件上传
基本概念
const file = event.target.files[0];读取的是文件基本信息,如文件名、文件类型(MIME 类型,text/html、image/jpeg、application/json都属于MIME类型)和文件的最后修改时间。它非常小,它是指向文件的引用,当我们将File对象添加到FormData实例并发送网络请求时,浏览器会负责读取文件的实际内容并将其与表单数据一起发送到服务器。
如果想要获取文件数据,需要使用FileReader。
inputElement.addEventListener('change', (event) => {
const file = event.target.files[0];
console.log('上传的文件:', file);
const reader = new FileReader();
reader.onload = (e) => {
const fileData = e.target.result;
// 在这里处理文件数据,例如上传到服务器
};
reader.onerror = (e) => {
console.error('读取文件时发生错误:', e);
};
reader.readAsArrayBuffer(file);
});
自定义FileLoader组件(设置预览图,进度条)
//selectFile指的是<input />元素。监听该dom的change事件。
doms.selectFile.onchange = function () {
const file = this.file[0];
//显示预览图,先读取图片
const reader = new FileReader();
reader.onload = (e) => {
doms.img.src = e.target.result;
};
//开始读取文件
reader.readAsDataURL(file);
//上传
upload(file, function (percent) {
//设置进度条
});
};
function upload(file, onProgress, onFinish) {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
const resp = JSON.parse(xhr.responseText);
console.log("我拿到服务器返回的数据了");
};
xhr.upload.onprogress = (e) => {
const percent = Math.floor((e.loaded / e.total) * 100);
onProgress(percent);
};
xhr.open("POST", "url地址");
const form = new FormData();
form.append("后端规定的key", file);
xhr.send(form);
}
大文件上传
不用读取文件,可以直接上传:
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
const chunkSize = 1024 * 1024 * 5; // 分片大小,这里设置为 5MB
const fileSize = file.size;
const numberOfChunks = Math.ceil(fileSize / chunkSize);
let currentChunk = 0;
function uploadNextChunk() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, fileSize);
const chunk = file.slice(start, end);
// 在这里处理文件数据,例如上传到服务器
uploadChunk(chunk);
// 如果还有更多分片,继续读取下一块
if (currentChunk < numberOfChunks - 1) {
currentChunk++;
uploadNextChunk();
}
}
// 上传文件分片
function uploadChunk(chunk) {
const formData = new FormData();
formData.append('file', chunk);
// 添加其他所需的表单字段,例如 chunkIndex, totalChunks 等
// 发送请求到后端服务器,例如使用 axios.post('your_upload_url', formData);
console.log("上传分片:", chunk);
}
// 开始上传第一块分片
uploadNextChunk();
});
- 触发inputElement的change事件,表示用户已经选择文件。
const chunk = file.slice(start, end);表示取Blob对象中start到end之间的数据reader.readAsArrayBuffer(chunk);表示读取这段文件数据- 每次读取完成,触发
reader.onload事件,进行文件上传。
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
const chunkSize = 1024 * 1024 * 5; // 分片大小,这里设置为 5MB
const fileSize = file.size;
const numberOfChunks = Math.ceil(fileSize / chunkSize);
let currentChunk = 0;
const reader = new FileReader();
// 读取完成后的回调
reader.onload = (e) => {
const fileData = e.target.result;
// 上传分片
uploadChunk(fileData);
// 如果还有更多分片,继续读取下一块
if (currentChunk < numberOfChunks - 1) {
currentChunk++;
readNextChunk();
}
};
// 读取指定范围的文件内容
function readNextChunk() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, fileSize);
const chunk = file.slice(start, end);
reader.readAsArrayBuffer(chunk);
}
// 上传文件分片
function uploadChunk(fileData) {
// 在这里处理文件数据,例如上传到服务器
console.log("上传分片:", fileData);
}
// 开始读取第一块分片
readNextChunk();
});
11.浏览器中常见的线程
这道题主要想问Render进程内有哪些线程:
1、GUI渲染线程
负责渲染浏览器页面,解析HTML、CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被刮起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
2、JS引擎线程
JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;JS引擎线程一直等待着任务队列中任务的到来,然后加以处。
注意:GUI渲染线程与JS引擎线程的互斥关系,所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3、事件触发线程
比如,执行到这一行代码button.addEventListener('click',()=>{console.log("click"));的时候,会将回调函数添加到事件触发线程。当满足触发条件时,事件触发线程把该函数存入宏任务队列,等待JS引擎线程来执行。
4、定时器触发线程
比如执行到settimeout的时候,定时器线程会开始计时。当计时结束的时候,会将回调函数放入宏任务队列,等待JS引擎线程执行。
5、网络请求线程
回流和重绘
一句话:回流必将引起重绘,重绘不一定会引起回流。
layout是回流,Paint是重绘。
窗口大小改变、字体大小改变、以及元素位置改变,会引起回流。
颜色改变、透明度改变、visible会引起重绘。
12.浏览器页面之间通信若干方法
localstorage
核心要点:window.addEventListener("storage", (event)=>{})可以监听localstorage的变化。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sync Color Change</title>
</head>
<body>
<button id="changeColorBtn">Change Color</button>
<div
id="colorDiv"
style="width: 100px; height: 100px; background-color: white"
>
1
</div>
<script>
const changeColorBtn = document.getElementById("changeColorBtn");
const colorDiv = document.getElementById("colorDiv");
changeColorBtn.addEventListener("click", () => {
const newColor =
colorDiv.style.backgroundColor === "red" ? "white" : "red";
colorDiv.style.backgroundColor = newColor;
localStorage.setItem("divColor", newColor);
});
window.addEventListener("storage", (event) => {
if (event.key === "divColor") {
colorDiv.style.backgroundColor = event.newValue;
}
});
// 初始化颜色
const initialColor = localStorage.getItem("divColor") || "white";
colorDiv.style.backgroundColor = initialColor;
</script>
</body>
</html>
broadcastChannel
核心要点:
- 创建对象
const broadcastChannel = new BroadcastChannel('colorChannel'); - 发送消息
broadcastChannel.postMessage({ color: newColor }); }); - 监听消息
broadcastChannel.addEventListener
另一种常见的方法是使用 BroadcastChannel API。这个 API 允许在同一源(origin)的不同上下文(例如标签页、内嵌框架或workers)之间进行简单的通信。
- 创建一个 HTML 文件,其中包含一个按钮和一个 div。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sync Color Change with BroadcastChannel</title>
</head>
<body>
<button id="changeColorBtn">Change Color</button>
<div id="colorDiv" style="width: 100px; height: 100px; background-color: white;"></div>
<script src="script.js"></script>
</body>
</html>
- 在
script.js文件中,编写以下 JavaScript 代码:
const changeColorBtn = document.getElementById('changeColorBtn');
const colorDiv = document.getElementById('colorDiv');
// 创建一个 BroadcastChannel 实例
const broadcastChannel = new BroadcastChannel('colorChannel');
changeColorBtn.addEventListener('click', () => {
const newColor = colorDiv.style.backgroundColor === 'red' ? 'white' : 'red';
colorDiv.style.backgroundColor = newColor;
// 向其他上下文发送消息
broadcastChannel.postMessage({ color: newColor });
});
// 监听来自其他上下文的消息
broadcastChannel.addEventListener('message', (event) => {
if (event.data && event.data.color) {
colorDiv.style.backgroundColor = event.data.color;
}
});
websocket
WebSockets 是一种在浏览器和服务器之间建立持久连接的协议,允许双向实时通信。使用 WebSockets 可以实现跨多个浏览器标签页或窗口的同步。
实现步骤如下:
- 在服务器端,部署一个 WebSocket 服务器,负责处理客户端的连接和消息传输。
- 在浏览器端,为每个标签页或窗口创建一个 WebSocket 客户端,并连接到 WebSocket 服务器。
- 当用户在一个标签页中点击按钮,使 div 变为红色时,通过 WebSocket 客户端向服务器发送消息,例如:
{ type: "changeColor", color: "red" }。 - WebSocket 服务器接收到消息后,将其广播给所有已连接的 WebSocket 客户端。
- 其他标签页中的 WebSocket 客户端接收到消息,根据消息内容(在这种情况下是改变 div 颜色),执行相应的操作。
需要注意的是,实现这种方法需要在后端设置 WebSocket 服务器,对于简单的前端需求,这可能会显得有些繁琐。但是,对于需要实时通信和跨设备同步的场景,WebSockets 是一种非常有效的解决方案。
13. JavaScript中如何发送网络请求
XHR和fetch的优缺点
XMLHttpRequest优点:兼容性更好,早期浏览器也能支持。可以监听进度。
xhr.upload.addEventListener('progress', function (event) {
if (event.lengthComputable) {
var percentComplete = (event.loaded / event.total) * 100;
console.log('Upload progress:', percentComplete.toFixed(2) + '%');
}
}, false);
XMLHttpRequest缺点:语法繁琐。回调地狱
14. Reflect的用法
const obj = { a: 1, b: 2 };
console.log(Reflect.get(obj, 'a')); // 输出 1
const obj = { a: 1, b: 2 };
Reflect.set(obj, 'a', 10);
console.log(obj.a); // 输出 10
const obj = {};
const success = Reflect.defineProperty(obj, 'a', {
value: 1,
writable: true,
enumerable: true,
configurable: true
});
console.log(success); // 输出 true
console.log(obj.a); // 输出 1
15.Proxy常用陷阱
注意,修改proxy对象才会触发handler里面的方法。修改原始对象不会触发handler里的方法,但仍会修改proxy对象的值。
//劫持获取值
const target = { a: 1, b: 2 };
const handler = {
get(obj, prop) {
console.log(`Reading property "${prop}"`);
return obj[prop];
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.a); // 输出:Reading property "a" 和 1
console.log(proxy.b); // 输出:Reading property "b" 和 2
//劫持赋值操作
const target = { a: 1, b: 2 };
const handler = {
set(obj, prop, value) {
console.log(`Setting property "${prop}" to ${value}`);
obj[prop] = value;
},
};
const proxy = new Proxy(target, handler);
proxy.a = 3; // 输出:Setting property "a" to 3
proxy.b = 4; // 输出:Setting property "b" to 4
//劫持删除操作
const target = { a: 1, b: 2 };
const handler = {
deleteProperty(obj, prop) {
console.log(`Deleting property "${prop}"`);
delete obj[prop];
},
};
const proxy = new Proxy(target, handler);
delete proxy.a; // 输出:Deleting property "a"
console.log(target); // 输出:{ b: 2 }
16.生成器和迭代器
生成器执行返回的结果是迭代器
//下面是一个生成器
function* generatorFunc() {
yield 1;
yield 2;
yield 3;
}
//下面是一个迭代器
const generator = generatorFunc();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
用于实现异步编程
在没有aysc、await的时候,用迭代器和生成器实现异步
// 模拟异步操作的函数
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Hello, Generators and Promises!');
}, 1000);
});
}
// 生成器函数
function* myAsyncGenerator() {
const result = yield fetchData();
console.log(result);
}
// 辅助函数,用于执行生成器函数和处理异步操作
function runAsyncGenerator(generatorFunction) {
const iterator = generatorFunction();
function step(nextValue) {
const next = iterator.next(nextValue);
if (!next.done) {
next.value.then(step);
}
}
step();
}
// 运行生成器函数
runAsyncGenerator(myAsyncGenerator);
17. async和await
aysnc用于声明一个异步函数,它将异步函数转化为promise对象。内部遇到await会等待函数执行完毕。
// 声明一个异步函数
async function myAsyncFunction() {
// 进行一些异步操作,例如获取数据
// ...
// 返回一个值
return 'Hello, async/await!';
}
// 调用异步函数
myAsyncFunction()
.then((result) => {
// 当异步函数返回时,这里的代码会执行
console.log(result); // 输出: Hello, async/await!
})
.catch((error) => {
// 如果异步函数抛出错误,这里的代码会执行
console.error(error);
});
await 只能在 async 函数内部使用。如果 await 后面的表达式不是 Promise,它会将该表达式的值包装成一个已解析的 Promise。Promise.reolve()
19.内存泄漏
全局变量
在函数执行完后,leakyVariable仍然会存在于内存中。注意,如果用var、let、const都不会出现内存泄漏,因为这些变量都只在函数作用域内存在。
function leakyFunction() {
leakyVariable = "I'm a global variable.";
}
leakyFunction();
未移除事件监听器
每次mount组件的时候,给 window 添加一个回调函数。
componentDidMount(){
window.addEventListener("resize", this.onResize);
}
componentWillUnmount(){
// 忘记remove EventListener
}
## DOM的引用
```js
var refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, "refA"); // 但是还存在引用
能console出整个div 没有被回收
闭包
function outerFunction() {
const largeArray = new Array(1000000).fill('large data');
return function innerFunction() {
console.log(largeArray.length);
};
}
const closure = outerFunction();
// 由于闭包引用了 largeArray,largeArray 对象无法被垃圾回收,导致内存泄漏。
20.垃圾回收
引用计数法
如果没有引用指向该对象,那么该对象将会被回收。
假设不把otherObject设置为null,那么它仍会引用o.a。因此,o引用的对象不会被回收。
var o = {
a: {
b: 2,
},
};
var otherObject = o.a;
o = null;
otherObject = null; // 这里将引用计数降为0
缺陷:循环引用导致垃圾回收失效
function foo(){
var obj1 = {};
var obj2 = {};
obj1.x = obj2 ; // obj1引用obj2
obj2.x = obj1 ; // obj2引用obj1
return true ;
}
foo();
标记清除法
从root开始,即window变量,递归遍历它的子对象。每遍历一个对象,对该对象进行标记。遍历结束后,所有未被标记的对象即是垃圾对象,可以被回收。
弱引用
zhuanlan.zhihu.com/p/366505417
weakMap、weakSet、weakRef。其中 weakMap 的键(key)是弱引用,且必须是对象。弱引用的意思就是,这个 key 指向的对象,如果没有其他引用指向它,那么它就算作垃圾,可以被回收。
应用场景:再以下例子中,使用parent.removeChild(myElement) 之后,下一次垃圾回收,会将 myElement 所指向的对象回收。如果使用Map,则该对象不会被回收。因为它被 Map 的key引用。
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
事件循环
事件循环(初稿版)
根据目前所学,进入事件队列有以下几种:
- setTimeout的回调,宏任务。
- setInterval的回调,宏任务。
- Promise的then函数回调,微任务。
- requestAnimationFrame的回调,宏任务。
- 事件处理函数,宏任务
- 执行同步代码(全局代码)
- 检查微任务队列,执行所有微任务
- 执行任务队列中的一个任务
- 检查微任务队列,执行所有微任务
- 返回第3步,直到任务队列为空
(详细版)
- 宏任务:包括脚本整体、
setTimeout、setInterval、setImmediate(Node.js 环境)等。宏任务会被添加到任务队列中,按照顺序执行。 - 微任务:包括
Promise的回调(then、catch、finally)、MutationObserver的回调和async/await(实际上是Promise的语法糖)等。微任务会被添加到微任务队列中,在当前宏任务执行完毕后立即执行。
为什么区分宏任务和微任务?
因为每个任务的优先级不同,为了让高优先级的任务先执行,要通过宏任务和微任务来区分。
严格模式和非严格模式
this区别
严格模式的函数,如果没有绑定this,那么就是undefined;非严格模式是window
"use strict";
function logThis() {
console.log(this);
}
logThis(); // 输出: undefined
未声明变量
严格模式下禁止使用未声明的变量,非严格模式下如果使用了非严格对象,就是undefined,如果给它赋值,就是一个全局变量
箭头函数
简洁的语法
function add(num) {
return num + 10
}
const add = num => num + 10;
箭头函数的this
捕获定义时的上下文this。
// 情况1:如果外层有函数,那么箭头函数的this就是外层函数的this
let obj = {
fn:function(){
console.log('我是普通函数',this === obj) // true
return ()=>{
console.log('我是箭头函数',this === obj) // true
}
}
}
console.log(obj.fn()())
// 情况2:如果外层没有函数,那么箭头函数的this就是全局this,浏览器中是window,node中是global
let obj = {
fn:()=>{
console.log(this === window);
}
}
console.log(obj.fn())
// true
不能作为构造函数
没有arguments
let f = ()=>console.log(arguments);
//报错
f(); // arguments is not defined
immutable
是什么
一种特殊对象,一旦创建之后无法再修改原始对象;每次修改属性之后,都会返回一个全新对象。
优点:属性可以复用,比如修改obj的a属性,返回一个全新的obj。虽然新obj和旧obj是不同的对象,但是属性可以复用,比如b属性、c属性在内存中都是同样的地址。
怎么用
在React中,通过 shouldComponentUpdate()来判断属性是否需要更新,如果是普通对象,可能需要深度比较(也就是比较属性的属性是否是同一对象)。但是immutable的 is 方法可以直接进行比较 2 个 immutable对象是否是同一个对象。
其次,在Redux中,reducer处理state的时候,每次不能修改原始state,需要新创建一个state,所以需要深拷贝(比如lodash)。如果将state设置为immutable对象,则每次直接修改即可,因为肯定会返回一个新对象。
装饰器是什么
装饰器是一个函数的语法糖
可以看出 moveDecoator 本质就是一个函数,他给 target 的 prototype 添加函数或者属性,实现继承。功能和 extends 一样,但是装饰器可以连续使用,实现多个继承。
const moveDecoator: ClassDecorator = (target:Function) => {
target.prototype.name = "codereasy";
target.prototype.getPosition = () => {
return "hello";
}
}
@moveDecoator
class Tank{}
const t = new Tank()
t.getPosition()
t.name;
方法装饰器
从图中可知,参数1:方法的原型对象;参数2:字符串,方法名。参数3:配置项:是否可写,可枚举等等。这里的value就是方法本身(本质就是一个函数)
ESModule和CommonJs的区别
1.CommonJs是导出值的拷贝,ES6是导出值的引用;
2.CommonJs是运行时加载,因为require可以写在if语句内。ES6是静态编译,import不能写在if内。
3.处理循环引用时,CommonJs是先将导出内容存放到一块新内存(缓存),因此下一次导入a的时候,直接从缓存中取。(缓存中不一定是最新值)。而ES6则是导出的引用(内存地址),因此获取的一定是最新值。
Webwroker
参考:www.bilibili.com/video/BV1n2…
总结:
-
在主线程中
new webWorker()对象。 -
在主线程中,调用
worker.postMessage()给子线程发送消息。 -
此时会执行子线程中的代码,在子线程中,通过postMessage将执行结果传给主线程。
-
主线程通过
worker.addEventListener('message')来监听子线程传递的消息。
作用域
1. 全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。例如:
// 全局变量
var greeting = 'Hello World!';
function greet() {
console.log(greeting);
}
// 打印 'Hello World!'
greet();
2. 函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。例如:
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);
3. 块级作用域
ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。看例子:
{
// 块级作用域中的变量
let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);
词法作用域
词法作用域(也叫静态作用域)从字面意义上看是说作用域在词法化阶段(通常是编译阶段)确定而非执行阶段确定的。看例子:
let number = 42;
function printNumber() {
console.log(number);
}
function log() {
let number = 54;
printNumber();
}
// Prints 42
log();
上面代码可以看出无论printNumber()在哪里调用console.log(number)都会打印42。动态作用域不同,console.log(number)这行代码打印什么取决于函数printNumber()在哪里调用。
Map和大括号的区别
key值不同
对象的 key 是字符串,如果不是字符串会调用 tostring()转化为字符串。map 的 key 没有限制。
对象有继承
自动继承了 Object.prototype(除非使用 Object.create(null)).所以 obj.属性可能访问的是原型链上的属性(hasOwnProperty、toString、constructor)
速度对比
Map 比 Object 快,在插入、迭代和删除数据的速度上。
大图片加载
预加载
使用link标签进行预加载
<link rel="preload" href="./img/all.jpg" as="image" />
图片拆分
搜一个图片拆分工具就可以,很多在线的网页。
onload
- 最开始是一个
loading图片。
<img src="loading.gif" id="loadingImage" alt="Loading" />
- new 一个 Image 对象,它不会展示。
const realImage = new Image();
realImagesrc = "real-image.jpg";
- 监听 Image 对象加载完成,替换真实图片的 src。
realImage.onload = function() {
// 图片加载完成后,替换 `loading` 图片为真实图片
const loadingImage = document.getElementById("loadingImage");
loadingImage.src = realImage.src;
};