问:0.1 + 0.2 === 0.3 嘛?为什么?
JavaScirpt 使用 Number 类型来表示数字(整数或浮点数),遵循 IEEE 754 标准0.1 -> 0.0001100110011001...(无限循环)0.2 -> 0.0011001100110011...(无限循环)
参考链接: juejin.cn/post/684490…
所以总结1::先按照IEEE 754转成相应的二进制,然后对阶运算 ,精度丢失可能出现在进制转换和对阶运算中
总结2:简单地说,十进制来说1/3这个分数换成小数是0.33333…,然后储存位数是有限的,所以会出现误差,
然后对 应到计算机这边来,将数字转成2进制保存的时候,很多数变成无限不循环小数
,也就是说,数字在保存的时候存在一部 分失真,然后加减之后就可能出现精度问题了
构造函数面试题
function Person(name) {
this.name = name; // 这行代码会执行,但结果会被丢弃
return { name: 'Override' }; // 显式返回一个对象
}
const p = new Person('Alice');
// p 接收的是返回的 { name: 'Override' } 而不是 this 对象
console.log(p.name); // 输出 "Override"
function Foo(){
let age = 25 // 私有属性
getName=function(){ // 私有方法
console.log(1)
}
this.name = name // 实例属性
this.run = function() { // 实例方法
console.log('run')
}
return this;
}
function getName(){ // 函数 函数声明(注意是函数声明,不是函数表达式或者构造函数创建函数)
console.log(5)
}
Foo.getName=function(){ // 静态方法
console.log(2)
}
Foo.prototype.getName=function(){ //原型方法
console.log(3)
}
getName=function(){ // 函数表达式
console.log(4)
}
Foo.getName() //2 静态方法
getName()//4 // 函数表达式覆盖函数声明
Foo().getName() //1 函数私有方法
new Foo.getName() // 2 静态方法
new Foo().getName() //3 原型方法
new new Foo().getName() // 3
相当于new(new Foo().getName())先执行new Foo().getName()由6部知道了输出3,
再创建Foo.prototype.getName()的实例返回。结果为3
注意把私有属性和静态属性区分开,把实例属性和原型属性区分开。
详细解析参考:juejin.cn/post/684490…
function aa(){
return 123
}
var aa=8888
console.log(aa)结果 8888
函数和变量命名冲突时候,函数的优先级高,但是最后赋值的是变量
js数据类型分
基本类型+引用类型
基本类型:
null,undefined,boolean,number,string,symbol
引用类型:
array Obj
let var const 区别
1. 作用域
- var:函数作用域(在函数内部声明则为局部变量,否则为全局变量)
- let 和 const:块级作用域(
{}内有效)
2. 变量提升
- var:会提升变量的声明(初始化为
undefined) - let 和 const:也会提升,但存在暂时性死区(TDZ),在声明前访问会报错
console.log(a); // undefined(变量提升)
var a = 1;
console.log(b); // ReferenceError(暂时性死区)
let b = 2;
3. 重复声明
- var:允许在相同作用域内重复声明
- let 和 const:不允许重复声明
var x = 1;
var x = 2; // 允许
let y = 1;
let y = 2; // SyntaxError
const z = 1;
const z = 2; // SyntaxError
4. 初始化和修改
-
var 和 let:声明后可以重新赋值
-
const:
- 声明时必须初始化
- 基本类型的值不可修改
- 引用类型的内部属性可以修改,但无法重新赋值
5. 全局对象属性
- 在全局作用域中使用 var 声明变量会成为
window对象的属性 - let 和 const 不会
关于没有赋值返回情况
1、var、let、const 在声明但不赋值时:
- var
var a;
console.log(a); // undefined
console.log(typeof a); // "undefined"
// 变量提升的情况
console.log(b); // undefined(变量提升)
var b;
- let
let x;
console.log(x); // undefined
console.log(typeof x); // "undefined"
// 注意:必须先声明才能访问
// console.log(y); // ReferenceError(暂时性死区)
// let y;
- const 不允许声明不赋值
const z; // SyntaxError: Missing initializer in const declaration
// 必须声明时立即赋值
const correct = "value";
console.log(correct); // "value"
- 对比表格
| 关键字 | 允许声明不赋值 | 默认值 | 暂时性死区 | 重复声明 |
|---|---|---|---|---|
var | ✅ 允许 | undefined | ❌ 无 | ✅ 允许 |
let | ✅ 允许 | undefined | ✅ 有 | ❌ 不允许 |
const | ❌ 不允许 | - | ✅ 有 | ❌ 不允许 |
2、 函数没有 return 语句 → 返回 undefined
3、数有 return;(没有值) → 返回 undefined
4、对象的属性没有赋值,返回undefined
undefined 与 null 的区别
| 方面 | undefined | null |
|---|---|---|
| 本质 | 表示"缺少值" | 表示"空对象引用" |
| 产生方式 | 系统自动 | 程序员手动 |
| typeof | "undefined" | "object"(bug) |
| 转换为数字 | NaN | 0 |
| 转换为布尔 | false | false |
| JSON序列化 | 被省略 | 保留为 null |
| 相等比较 | undefined == null 为 true | null == undefined 为 true |
| 全等比较 | undefined === null 为 false | null === undefined 为 false |
简单记忆:
undefined:系统说"我还没准备好值"null:程序员说"这里就是空的"
函数和箭头函数区别
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
| this 指向 | 动态绑定(调用时确定) | 词法绑定(定义时确定) |
| arguments 对象 | ✅ 有 | ❌ 无 |
| 构造函数 | ✅ 可以(用 new) | ❌ 不可以 |
| 原型 prototype | ✅ 有 | ❌ 无 |
| yield 关键字 | ✅ 可以用 | ❌ 不能用(除非嵌套) |
| 函数名 | 可选命名 | 匿名(可通过变量名访问) |
1、this 指向
事件处理器的经典问题 const button = document.querySelector('button'); // ❌ 普通函数 - this 可能丢失 button.addEventListener('click', function() { console.log(this); // button 元素(正确)
setTimeout(function() {
console.log(this); // window(错误!)
}, 100);
});
// ✅ 箭头函数 - this 保持不变
button.addEventListener('click', function() {
console.log(this); // button 元素
setTimeout(() => {
console.log(this); // 仍然是 button 元素(正确!)
}, 100);
});
对象方法中的 this
const obj = {
name: 'obj',
regularFunc: function() {
console.log(this.name); // 取决于调用方式
},
arrowFunc: () => {
console.log(this.name); // 永远是定义时的 this(通常是 window)
}
};
obj.regularFunc(); // 'obj'
obj.arrowFunc(); // undefined(this 指向外层作用域)
2. arguments 对象
普通函数:有 arguments
function sum() {
console.log(arguments); // 类数组对象
console.log(Array.from(arguments)); // 转为真数组
return Array.from(arguments).reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 6
箭头函数:无 arguments(使用剩余参数)
const sum = (...args) => {
// console.log(arguments); // ReferenceError: arguments is not defined
console.log(args); // 真数组
return args.reduce((a, b) => a + b, 0);
};
console.log(sum(1, 2, 3)); // 6
3. 构造函数和 new
普通函数:可作为构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
const john = new Person('John', 25);
console.log(john.greet()); // "Hello, I'm John"
console.log(john instanceof Person); // true
箭头函数:不可作为构造函数
const Person = (name, age) => {
// 没有自己的 this,也没有 prototype
// this.name = name; // 这里的 this 是外层的 this
};
// ❌ 会抛出错误
// const john = new Person('John', 25);
// TypeError: Person is not a constructor
console.log(Person.prototype); // undefined
4. 原型 prototype 属性
// 普通函数有 prototype
function regularFunc() {}
console.log(regularFunc.prototype); // {constructor: ƒ}
// 箭头函数没有 prototype
const arrowFunc = () => {};
console.log(arrowFunc.prototype); // undefined
5. yield 关键字(生成器)
普通函数可以作为生成器
function* regularGenerator() {
yield 1;
yield 2;
yield 3;
}
箭头函数不能作为生成器
// const arrowGenerator = *() => {}; // SyntaxError
// 但箭头函数可以在生成器内部使用
function* outerGenerator() {
const innerFunc = () => {
// 这里可以用箭头函数
};
yield 1;
}
==和===区别
JavaScript 中用于比较的运算符,它们在比较时的严格程度不同。
// 类型不同时,会尝试转换为相同类型再比较
console.log(5 == "5"); // true (字符串 "5" 转换为数字 5)
console.log(true == 1); // true (true 转换为数字 1)
console.log(false == 0); // true (false 转换为数字 0)
console.log(null == undefined); // true (特殊情况)
console.log("" == 0); // true (空字符串转换为数字 0)
console.log(" " == 0); // true (空格字符串转换为数字 0)
===(严格相等)
比较规则:不进行类型转换,类型不同直接返回 false。
// 类型不同直接返回 false
console.log(5 === "5"); // false (数字 vs 字符串)
console.log(true === 1); // false (布尔值 vs 数字)
console.log(false === 0); // false (布尔值 vs 数字)
console.log(null === undefined); // false (不同类型)
console.log("" === 0); // false (字符串 vs 数字)
特殊比较
console.log(NaN == NaN); // false (特殊规则)
console.log(NaN === NaN); // false (特殊规则)
// 判断 NaN 的正确方式
console.log(isNaN(NaN)); // true
console.log(Number.isNaN(NaN)); // true (更安全)
对象比较
const obj1 = { name: "John" };
const obj2 = { name: "John" };
const obj3 = obj1;
console.log(obj1 == obj2); // false (不同引用)
console.log(obj1 === obj2); // false (不同引用)
console.log(obj1 == obj3); // true (相同引用)
console.log(obj1 === obj3); // true (相同引用)
拓展
console.log(![]); // false
console.log(!![]); // true
console.log(!{}); // false
console.log(!""); // true
console.log(!"hello"); // false
console.log(!0); // true
console.log(!1); // false
数组比较
[] == false //结果: true (空数组转换为0,false也转换为0)
[] === false // false 类型不同
[]==[]// false 两个不同的对象 - 只有当两个变量指向内存中的同一个对象时,比较才返回 `true`
-
[]===[] // false
![]==[] //true。
这些结果看起来矛盾是因为:
[]在布尔上下文中是true如![] 转换为!true就是false[]在相等比较中会转换为0![]得到falsefalse在相等比较中也会转换为0
[] == false // true (空数组转换为0,false也转换为0)
![]== false // true
![]== [] //true
[0] == false //true("0" == 0)
[1] == false // false ("1" == 0)
四则运算符
加法运算时: 其中一方是字符串类型,就会把另一个也转为字符串类型
其他运算 :其中一方是数字,那么另一方就转为数字
1 + '1' // '11'
2 * '2' // 4
什么是闭包?(必考)
回答1:
闭包的实质是因为函数嵌套而形成的作用域链
比如说:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包
用途:使用闭包主要是为了设计私有的方法和变量
优点:可以避免变量被全局变量污染
缺点:函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包
解决方法:在退出函数之前,将不使用的局部变量全部删除
回答2:
在js中函数可以构成闭包,根据作用域链规则 函数访问只允许外部数据,外部不能访问内部数据,闭包了解决外界允许访问,可以间接的通过一个函数返回新的函数或者对象的方法
回答3:
闭包就是有权限访问另一个函数作用域的变量函数
经典题目:
for(var i=0;i<5;i++){
setTimeout(()=>{
console.log(i)
},10)
}
答案:55555
利用闭包原理:
for(var i=0;i<5;i++){
((j)=>{
setTimeout(()=>{
console.log(j)
},10)
})(i)
}
答案:0 1 2 3 4
利用setTimeOut第三个参数传参
for(var i=0;i<5;i++){
setTimeout((j)=>{
console.log(j)
},10,i)
}
答案:0 1 2 3 4
let 块作用域
for(let i=0;i<5;i++){
setTimeout(()=>{
console.log(i)
},10,i)
}
答案:0 1 2 3 4
立即执行函数
1、(function(){ ... })()
2、(function(){ ... }())
javascript 中没有私有作用域的概念,使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以 (function(){ … })() 内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”。
函数的分类
函数声明:
function fnName() {...} ;
fnName();
函数表达式
使用 function 关键字声明一个函数,但未给函数命名,最后将匿名函数赋予一个变量,叫函数表达式,这是最常见的函数表达式语法形式。
var fnName = function() { ... } ;
匿名函数:
实现闭包 , 模拟块级作用域,减少全局变量
匿名函数属于函数表达式,匿名函数有很多作用,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序或创建闭包等等。
function() { ... } ;
单独运行一个匿名函数,不符合语法要求,执行会报错。 解决方法就是:
(function (str){
//此时会输出jj好帅!
console.log("yj"+str);
})("好帅!")
var name = 'world';
(function()
{
console.log(name); // 1
console.log(this.name) // 2
if(typeof name === 'undefined')
{
var name = 'jack'
console.log('goodbye ' + name);
}
else
{
console.log('Hello'+name);
}
})();
答案 1 : undefined 原因: 下面的代码中 有 var 声明变量 name , 而后变量提升
所以 name 为 undefined
答案2 : world 原因:匿名函数内的 this 指向全局的 window 对象 , 而外层 var 会在 window 对象上创建,
答案3 :goodbye jack 原因如第一条
案例二:
但是 如果我们使用 let 进行定义呢?
let name = 'world';
(function()
{
console.log(name); // 1
console.log(this.name) // 2
if(typeof name === 'undefined')
{
let name = 'jack'
console.log('goodbye ' + name);
}
else
{
console.log('Hello'+name);
}
})();
答案1 : world 原因:不存在变量提升时 匿名函数内的 name 直接访问到了 外部作用域的 name
答案2 :undefined 原因:let 并不是定义在window 对象上的 而匿名函数中的this指向的是外部的window 对象,所以并没有打印出来东西
答案3 :走else 语句 : hello world
案例三:
var name = 'world'; (function() { console.log(name); // world console.log(this.name) // world
if(typeof name === 'undefined')
{
let name = 'jack'
console.log('goodbye ' + name);
}
else
{
console.log('Hello'+name);//hello world
}
})();
案例四:
let name = 'world'; (function() { console.log(name); // undefined console.log(this.name) // undefined
if(typeof name === 'undefined')
{
var name = 'jack'
console.log('goodbye ' + name);
}
else
{
console.log('Hello'+name);
}
})();
浅拷贝深拷贝
拷贝引用类型时候,浅拷贝的针地址还是指向同一个,深拷贝不是,是全新的对象
浅拷贝
方法一: 利用 Object.assign 枚举
var a={
age:18
}
var b=Object.assign({},a);
a.age=28;
console.log(b.age) // 18
方法二: 利用 展开运算符(…)来解决
var a={age:18}
var b={...a};
a.age=28;
console.log(b)//18
深拷贝
方法一:遍历封装
function clone(o1){
var temp={}
for(var k in o1){
if(typeof(o1[k])=='object'){
temp[k]=clone(o1[k])
}else{
temp[k]=o1[k];
}
}
return temp;
}
var ojb1={
age:25,
say:[
{one:'1'},
{two:'2'}
]
}
var ojb2=clone(ojb1)
ojb1.age=28
console.log(ojb1,ojb2)
方法二:如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
var obj = {a: 1, b: {
c: b
}}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()
原始方法【不推荐】
这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。 有局限性的:
- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE`
export default 和export 和 module.exports 和 exports 区别
module.exports和exports是属于 CommonJS 模块规范,export和export default是属于ES6语法。
module.exports和exports导出模块,用require引入模块。
export和export default导出模块,import导入模
import from 和require 、import()区别
import from Es6导入模块 require 是 CommonJS 到导入模块 import() 是动态导入模块:(1)按需加载。
import()函数在ES2020提案 中引入,他可以异步动态加载模块
if(true){
return import('./xxx/aaa').then(msg=>{
//加载内容 不会报错
}).catch(err=>{
//error codo
})
}
由于它动态加载等特性,可以在一些场合很好的使用:
1. vue项目路由按需加载
{
path:'/xxx'
name:'/XXX'
component:()=>import(../xxx/xxx.vue)
}
2.模块的按需加载
xxx.click=function(){
import('../xxx').then(fn=>{
...
})
}
复制代码
3.条件加载
if(true){
return import('./xxx/aaa').then(msg=>{
//加载内容
}).catch(err=>{
//error codo
})
}
还可以配合promise的方法.all方法进行多个模块的加载, 还有很多的有趣用法这里就不一一列举了。
export和 exprot default
在创建JavaScript模块时,export 语句用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。被导出的绑定值依然可以在本地进行修改。在使用import进行导入时,这些绑定值只能被导入模块所读取,但在export导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。
- 1: 导出实时绑定的函数、对象或原始值,其他程序通过import 使用
- 2: 绑定的值只能被导入模块读取
- 3:export导出模块中对这些绑定值进行修改,所修改的值也会实时地更新
如:a.js文件下
export function getCookie(keys) {
var values = '';
var cookie = document.cookie;
var cookie_arr = cookie.split(";");
for (var i = 0; cookie_arr[i]; i++) {
var c_arr = cookie_arr[i].split('=');
var _key = c_arr[0].replace(' ', '');
if (_key == keys) {
values = c_arr[1];
break;
}
}
return values;
}
b.js文件导入使用
import {getCookie} from "@/config/utils.js";
两种 exports 导出方式:
命名导出(每个模块包含任意数量)
默认导出(export defaul每个模块包含一个)
// 导出单个特性
export let name1, name2, …, nameN; // also var, const
export let name1 = …, name2 = …, …, nameN; // also var, const
export function FunctionName(){...}
export class ClassName {...}
// 导出列表
export { name1, name2, …, nameN };
// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };
// 解构导出并重命名
export const { name1, name2: bar } = o;
// 默认导出
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
// 导出模块合集
export * from …; // does not set the default export
export * as name1 from …; // Draft ECMAScript® 2O21
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;
在导出多个值时,命名导出非常有用。在导入期间,必须使用相应对象的相同名称。
但是,可以使用任何名称导入默认导出,例如:
// 文件 test.js
let k; export default k = 12;
// 另一个文件
import m from './test'; // 由于 k 是默认导出,所以可以自由使用 import m 替代 import k
console.log(m); // 输出为 12
你也可以重命名命名导出以避免命名冲突:
export { myFunction as function1,
myVariable as variable };
例子
// childModule1.js 中
let myFunction = ...; // assign something useful to myFunction
let myVariable = ...; // assign something useful to myVariable
export {myFunction, myVariable};
// childModule2.js 中
let myClass = ...; // assign something useful to myClass
export myClass;
// parentModule.js 中
// 仅仅聚合 childModule1 和 childModule2 中的导出
// 以重新导出他们
export { myFunction, myVariable } from 'childModule1.js';
export { myClass } from 'childModule2.js';
// 顶层模块中
// 我们可以从单个模块调用所有导出,因为 parentModule 事先
// 已经将他们“收集”/“打包”到一起
import { myFunction, myVariable, myClass } from 'parentModule.js'
区别:
export default是唯一的,导出没有{},如export default a,导入:import a from export可以是多个,且要加{},如export {a,b},导入:import {a,b} from
module.exports 和exports区别
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即 module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
exports其实是module.exports的引用 ,可以直接在exports对象上添加相关的方法。
1)使用module.exports导出模块:
新建一个文件demo.js,通过module.exports输出变量x和函数add。
var x = 1;
var add = function (val) {
return val + x;
};
module.exports.x = x;
module.exports.add = add;
(2)使用require引入模块
require方法用于加载模块。
var demo = require('./demo.js');
console.log(demo.x); // 1
console.log(demo.add(1)); // 6
例子
1:a.js文件中方法导出
function formatTime(date) {
var year = date.getFullYear()
var month = date.getMonth() + 1
var day = date.getDate()
var hour = date.getHours()
var minute = date.getMinutes()
var second = date.getSeconds()
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
module.exports = {
formatTime: formatTime
}
2:b.js把a.js引入并使用
var Util = require('../a.js');
Util.formatTime() //调用
区别:
(1)module.exports和exports的用法是后面加一个等号,再接具体的导出
module.exports=... exports=...
防抖和节流
防抖动是将多次执行变为最后一次执行
场景1:如:规定时间300ms 一个输入框,连续输入时候,输入间隔(即是停留时间)在300ms内视为同一次,不执行函数,,如果突然停留大于300ms就会执行一次函数
场景2,鼠标移动,规定时间300ms,我在指定的区域移动,连续移动(鼠标没有停留小于300ms,视为同一次不执行函数,如果突然停留大于300ms就会执行一次函数
场景:
- input 输入框请求事件
- 鼠标移动事件
- 滚动条滚动事件onsroll
- 浏览器窗口变大缩小事件 resize事件
节流是将多次执行变成每隔一段时间执行。
防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。
防抖
1:方法一:
短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。
非立即执行版:
非立即执行版
function debounce(func ,wait){
let timer;
return function(){
let contextThis=this;
let args=arguments;
if(timer) clearTimeout(timer)
timer=setTimeout(()=>{
func.apply(this,args)
},wait)
}
}
非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间:
立即执行版:
function debounce(func ,wait){
let timer;
return function(){
let contextThis=this;
let args=arguments;
if(timer) clearTimeout(timer)
let callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) func.apply(contextThis, args);
}
}
立即执行版立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。
合并
function debounce(func ,wait,immediate){
let timer;
return function(){
let contextThis=this;
let args=arguments;
if(timer) clearTimeout(timer)
if (immediate) {
let callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) func.apply(contextThis, args);
}else{
timer=setTimeout(()=>{
func.apply(this,args)
},wait)
}
}
}
拓展有方法值和取消事件
function debounce(func,wait,immediate){
let timer,result;
let debouncefun=function(){
let __this=this;
let args=arguments;
if(timer) clearTimeout(timer);
if(immediate){
let callNow=!timer;
timer=setTimeout(()=>{
timer=null
},wait)
if(callNow) func.apply(__this,args)
}else{
timer=setTimeout(()=>{
func.apply(this,args)
},wait)
}
return result; // 返回最后的结果
}
// 添加取消功能
debouncefun.cancel=function(){
clearTimeout(timer);
timer=null;
}
return debouncefun
}

节流
防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
let content2 = document.getElementById('content2');
function count2() {
content2.innerHTML = num++ +'Throttle节流';
};
content2.onmousemove = throttle(count2,1000,true);
// // 时间戳版
function throttle(func ,wait){
let preTime=0;
return function(){
let now =Date.now()
let __this=this;
let args=arguments;
if(now-preTime>wait){
func.apply(__this,args)
preTime=now
}
}
}
// 定时器版
function throttle(fun,wait){
let timer;
return function(){
let __this=this;
let args=arguments;
if(!timer){
timer=setTimeout(()=>{
fun.apply(__this,args)
timer=null
},wait)
}
}
}
call, apply, bind 区别


apply、call、bind共同点
apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
apply 、 call 、bind 三者都可以利用后续参数传参
apply、call、bind不同点
-
bind是偏函数型,返回对应函数,便于稍后调用;apply、call则是立即调用 。
-
call 立即调用,需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。例如:func.call(this, arg1, arg2);。(非严格模式下)当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。
-
apply 立即调用,把参数放在数组里。例如:func.apply(this, [arg1, arg2])。
bind
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
this.height = '155'; // 在浏览器中,this 指向全局的 "window" 对象
let user = {
height: '170',
getHeight: function() { return this.height; }
};
// 自身调用
user.getHeight(); //user调用getX,此时getX里的this指的是user
console.log(user.getHeight()); // '170'
// 赋值调用
let tianZhen = user.getHeight;【注意这里不是uers.getHeight()】 // 相当于 tianZhen = function() { return this.height; }
tianZhen(); // tianZhen函数是在全局作用域中调用的,相当于window.tianZhen(),此时tianZhen = function() { return this.height; }里的this指window
console.log(tianZhen()); // '155'
// 赋值绑定调用
let bindGetH = tianZhen.bind(user); // 把 'this' 绑定到 user 对象,此时bindGetH未立即执行。相当于 tianZhen = function() { return user.height; }
bindGetH(); // 即使在全局作用域直接调用,window.tianZhen()。function() { return user.height; },得到user里的height
console.log(bindGetH()); // '170'

bind实际用途
在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它,vue中组件dom绑定很少会用bind,但是在react中,使用React.createClass会自动绑定每个方法的this到当前组件,但使用ES6 class或纯函数时,就要靠手动绑定this了,而此时bind就非常合适。 像这样:
1.在dom添加方法时使用bind,并传入this(需要绑定this的上下文), <button onClick={this.test.bind(this)}>点我一下</button>
import React, {Component} from 'react'
class Greeter extends Component{
constructor (props) {
super(props)
this.state = {}
}
test (value) {
console.log(value)
}
render () {
return (
<div>
<button onClick={this.test.bind(this,3)}>点我一下</button>
</div>
)
}
}
2.在构造函数 constructor 内绑定this,好处是仅需要绑定一次,无需在dom中绑定,避免每次渲染时都要重新绑定,函数在别处复用时也无需再次绑定。constructor (props) { super(props) this.test=this.test.bind(this) }
import React, {Component} from 'react'
class Greeter extends Component{
constructor (props) {
super(props)
this.state = {}
this.test=this.test.bind(this)
}
test (value) {
console.log(value)
}
render () {
return (
<div>
<button onClick={this.test(3)}>点我一下</button>
</div>
)
}
}
bind()的连续调用 :多次bind() 是无效的。只有bind第一个起作用


apply、call
all 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。

apply、call 的区别
- call 立即调用,需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。例如:func.call(this, arg1, arg2);。(非严格模式下)当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。
- apply 立即调用,把参数放在数组里。例如:func.apply(this, [arg1, arg2])。 如:

使用用途:数组追加,最大小值、
数组上追加 push

获取数组中的最大值和最小值 maxth.max

验证是否是数组(前提是toString()方法没有被重写过)

类(伪)数组使用数组方法 slice方法

apply、call、bind比较
var obj = {x: 81,
};
var foo = {
getX: function() {
return this.x;
}
}
console.log(foo.getX.bind(obj)()); //81
console.log(foo.getX.call(obj)); //81
console.log(foo.getX.apply(obj)); //81
三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。
也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。
this指向(普通函数、箭头函数)是什么?
普通函数:看运行时情况
function show() {
console.log(this);
}
show();// 输出 window(非严格模式)
const obj = {
name: "obj",
log: function() {
console.log(this.name);
}
};
obj.log(); // 输出 "obj"
function greet() {
console.log(this.name);
}
const ctx = { name: "Alice" };
greet.call(ctx); // 输出 "Alice"
function Person(name) {
this.name = name;
}
const p = new Person("Bob");
console.log(p.name); // 输出 "Bob"
箭头函数
箭头函数 没有自己的 this,其 this 继承自定义时的 父级作用域(词法作用域),且 不可被修改:
const obj = {
name: "obj",
log: () => console.log(this.name) // this 指向全局 window
};
obj.log(); // 输出 undefined(假设全局无 name 属性)
class Timer {
constructor() {
this.value = 100;
}
start() {
// 箭头函数继承外层 this(Timer 实例)
setTimeout(() => console.log(this.value), 100); // 输出 100
}
}
写代码:实现函数能够深度克隆基本类型
function deepCopy(obj) {
if (typeof obj === 'object') {
var result = obj.constructor === Array ? [] : {};
for (var i in obj) {
result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i];
}
} else {
var result = obj;
}
return result;
}
从输入url到http协议请求交互过程
从url 》请求阶段》响应阶段》渲染阶段过程

构建请求
获取url信息
检查浏览器是否有强缓存
通过Cache-Control和Expires来检查是否命中强缓存,命中则直接取本地磁盘的html(状态码为200 from disk(or memory) cache,内存or磁盘);记忆缓存优先于磁盘缓存
DNS域名解析
没有命中强缓存,则把url 发送到DNS服务器,根据NDS协议**返回 域名所对应的 IP 地址, 同样还提供了 DNS 数据缓存服务。
等待 TCP 队列
对于HTTP1.1而言,Chrome使用同一个域名同时最多只能建立 6 个 TCP 连接,超出数量则需排队。
建立 TCP 连接
该阶段需要三次握手才能确认双方的接收与发送能力是否正常
1、第一次握手:客户端给服务器发送一个 SYN 报文。 2、第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。 3、第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。 4、服务器收到 ACK 报文之后,三次握手建立完成。
发起HTTP请求
向服务器发送一个HTTP请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。

服务器响应请求并返回html
服务器以一个状态行作为响应返回,响应的内容包括协议的版本、http状态、
服务器信息、响应头部和响应数据(返回html) 或者304缓存

HTTP 的状态码为三位数,被分为五类:
- 1xx: 表示目前是协议处理的中间状态,还需要后续操作。
- 2xx: 表示成功状态。
- 3xx: 重定向状态,资源位置发生变动,需要重新请求。
- 4xx: 请求报文有误。
- 5xx: 服务器端发生错误。
把html渲染到浏览器【执行浏览器渲染机制】
断开连接【TCP四次挥手】
但需要注意的是,如果浏览器或者服务器在其头信息中加入了如下代码,则不会断开连接。
Connection:Keep-Alive
HTTP协议
是超文本传输协议; HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。
http 和 https 的区别
http 传输的数据都是未加密的,也就是明文的,网景公司设置了 SSL 协议来对 http 协议传输的数据进行加密处理,简单来说
https 协议是由 http 和 ssl 协议构建的可进行加密传输和身份认证的网络协议,比 http 协议的安全性更高。
https是怎么保证安全的,为什么比http安全
-
加密传输:HTTPS使用SSL/TLS协议对HTTP报文进行加密,使得敏感数据在网络传输过程中不容易被窃听和篡改。这种加密过程结合了对称加密和非对称加密,确保数据的保密性和完整性。
-
身份验证:HTTPS通过数字证书进行身份验证,确保通信双方的真实性。在建立HTTPS连接时,服务器会提供数字证书来证明自己的身份。如果验证通过,客户端就可以信任服务器,并继续与其进行安全的数据传输。这有效防止了被恶意伪装的服务器攻击。
-
数据完整性保护:在传输数据之前,HTTPS会对数据进行加密,并使用消息摘要(hash)算法生成一个摘要值。在数据到达接收端后,接收端会使用相同的算法对接收到的数据进行摘要计算,并与发送端的摘要值进行比较。如果两者一致,说明数据在传输过程中没有被篡改。如果不一致,通信双方应重新进行验证或中断连接。
post请求为什么会多发送一次option请求
是HTTP 的特性“预检请求”,检查服务器是否允许来自不同源(域、协议或端口)、这样做可以确保客户端在发送实际请求之前,先得到服务器的明确许可。
HTTP的keep-alive是干什么的?
是HTTP的长连接,是一种通过重用TCP连接来发送和接收多个HTTP请求的机制。其主要作用包括、减少链接服务器的建立次数、提搞响应事件
-
减少连接建立开销:在没有keep-alive的情况下,每次HTTP请求都需要经过TCP三次握手建立连接,这会导致较大的延迟和资源消耗。而使用keep-alive,可以在一个TCP连接上发送多个HTTP请求,从而减少了建立连接的开销。
-
降低网络负载:每次建立和关闭连接时,都会消耗网络带宽和服务器资源。通过保持持久连接,可以减少连接的频繁建立和关闭,从而降低了网络负载和服务器负载。
-
提高性能和响应时间:由于避免了连接建立和关闭的开销,keep-alive可以提高请求的响应时间和整体性能。客户端可以在同一个连接上连续发送请求,而服务器也可以在保持连接的情况下更快地响应这些请求。
-
支持HTTP管道化:HTTP管道化是允许客户端在同一TCP连接中连续发送多个请求而无需等待每个请求的响应的技术。当与keep-alive结合使用时,可以进一步提高性能。
HTTP协议采用了请求/响应模型。
客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。
服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、 服务器信息、响应头部和响应数据。
HTTP 报文结构是怎样的?
客户端向服务端发起请求报文
请求行(request line): method url 协议版本
head
(空行)
body(data)
响应报文结构:
状态行 协议版本 返回状态 (如 http/1.1 200 ok)
head
(空行)
body(data|html)
有哪些请求方法?
- GET 获取资源
- HEAD 获取资源 信息
- POST 提交数据,上传数据
- PUT 修改数据
- DELETE 删除资源
- CONNECT 建立隧道
- OPTIONS 对资源的请求方法,用来跨域请求
- ERACE 追踪请求
URI的理解 :统一资源标识符
URI包含了URN和URL
HTTP 状态码?
RFC 规定 HTTP 的状态码为三位数,被分为五类:
- 1xx: 表示目前是协议处理的中间状态,还需要后续操作。
- 2xx: 表示成功状态。
- 3xx: 重定向状态,资源位置发生变动,需要重新请求。
- 4xx: 请求报文有误。
- 5xx: 服务器端发生错误。
200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
204 (无内容) 服务器成功处理了请求,但没有返回任何内容
301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302 Found:临时重定向,表示请求的资源临时搬到了其他位置
304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
400 (错误请求) 服务器不理解请求的语法。
401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
403 (禁止) 服务器拒绝请求。
404 (未找到) 服务器找不到请求的网页。
500 (服务器内部错误) 服务器遇到错误,无法完成请求。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,
301 和 302 的区别
301 是永久重定向:如使用域名跳转
302 是临时重定向:如未登陆的用户访问用户中心重定向到登录页面
304和200区别
304:如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的 内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。即客户端和服务器端只需要传输很少的数据量
HTTP状态码200扮演着关键角色。它表示服务器成功处理了请求,并返回了请求的资源。这一过程不仅涉及资源的传输,还关系到浏览器如何管理和利用缓存。具体来说,HTTP状态码200的请求资源主要包括首次请求、内存缓存、磁盘缓存等几种方式。
总结:
304:资源未变,使用缓存
200:请求成功、返回新资源
HTTP 1.0,1.1,2.0版本的区别
HTTP/1.0
- 1.0协议规定浏览器与服务器只保持短暂连接,请求结束就断开连接。
- 然减少了空连接的资源占用,但是增加了连接次数,连接的复用性降低,每次有新的请求就要重新建立一次连接。
- TCP连接的建立和断开需要[三次握手]、四次挥手,是一个很耗费时间的过程,每个连接又只能解决一次请求的通信,所以效率十分低下。
HTTP/1.1
- 在http1.0基础上默认开启了keepalive (长链接),客户端和服务器之间建立的连接可以复用认请求结束后保持连接一段时间 对方都没有新的请求,就可以主动断开连接,或者客户端在最后一个请求时,主动告诉服务端要断开连接
- 与http1.0相比,增加了管道机制,即在同一个** TCP连接中,客户端可以同时发送多个请求****,但是服务端还是按序响应**,并没有完全解决“队头阻塞
- 在http1.0的基础上,还添加了Host字段,这样就可以请求同一服务器的不同站点;增加了缓存机制、扩展了错误状态码、请求范围引入了range域.。
HTTP/2.0
-
为了解决1.1中存在的效率问题 ,http2.0采用了多路复用,客户端和浏览器都可以同时发送多个请求或响应,并且不用按照顺序一一对应,这是因为http2进行了二进制分帧(应用层和传输层之间的二进制分帧层),将传输信息分为更小的多个不同类型的帧,并进行标记,确保请求和响应的有序重组。
-
做了header压缩,和服务端推送(预先把一些热点等推送到缓存)
3:从输入url到页面展现发生了什么。:
第一种:dns解析域名为ip->建立tcp链接发起http请求-》http协商缓存-》浏览器渲染响应报文()
第二种:
1.输入域名地址
2.发送至DNS服务器并获得域名对应的WEB服务器IP地址;
3.与WEB服务器建立TCP连接,其中包括三次握手确定链接
4:发起http请求报文, 应并返回 对应的html
8.浏览器显示 HTML
9.浏览器发送请求获取的资源(如图片、音频、视频、CSS、JS等等)
10.浏览器发送异步请求
浏览器缓存机制:强制缓存和协商缓存
p6-juejin.byteimg.com/tos-cn-i-k3…
浏览器缓存分为强缓存和协商缓存
首先通过 Cache-Control 验证强缓存是否可用
-
如果强缓存可用,直接使用
-
否则进入协商缓存,即发送 HTTP 请求,服务器通过请求头中的
If-Modified-Since或者If-None-Match这些条件请求字段检查资源是否更新- 若资源更新,返回资源和200状态码
- 否则,返回304,告诉浏览器直接从缓存获取资源
这一节我们主要来说说另外一种缓存方式: 代理缓存。
强缓存:
浏览器不会像服务器发送任何请求,直接从本地缓存中读取文件并返回Status Code: 200 OK
强缓存主要使用Expires、Cache-Control 两个头字段,两者同时存在Cache-Control 优先级更高。当命中强缓存的时候,客户端不会再求,直接从缓存中读取内容,并返回HTTP状态码200。


200 form memory cache : 不访问服务器,一般已经加载过该资源且缓存在了内存当中,直接从内存中读取缓存。浏览器关闭后,数据将不存在(资源被释放掉了),再次打开相同的页面时,不会出现from memory cache。
200 from disk cache: 不访问服务器,已经在之前的某个时间加载过该资源,直接从硬盘中读取缓存,关闭浏览器后,数据依然存在,此资源不会随着该页面的关闭而释放掉下次打开仍然会是from disk cache。
优先访问memory cache,其次是disk cache,最后是请求网络资源
协商缓存:
协商缓存主要有四个头字段,它们两两组合配合使用,If-Modified-Since 和 Last-Modified一组,Etag 和 If-None-Match一组,当同时存在的时候会以Etag 和 If-None-Match为主。当命中协商缓存的时候,服务器会返回HTTP状态码304,让客户端直接从本地缓存里面读取文件。
参考链接:
juejin.cn/post/684490…
juejin.cn/post/705252…
流程
第一步,第一次发起 Ajax 请求,比如 GET /api/data。服务器处理后,会在响应头里加上两个关键标识:一个是 Last-Modified,值是这个接口返回的数据最后修改的时间,比如 “Wed, 23 Dec 2025 10:00:00 GMT”;另一个是 ETag,值是数据的唯一哈希值,比如 “5f8d02a7-1234”。同时,服务器返回 200 状态码和具体的数据,浏览器会把数据、Last-Modified、ETag 都存到本地缓存里。
第二步,过了一段时间,再次发起同样的 GET /api/data 请求。这时候,浏览器会先检查本地缓存,发现这个请求有缓存,而且强缓存已经过期(比如 Cache-Control 设置的 max-age 到时间了),就会在请求头里带上两个字段:一个是 If-Modified-Since,值就是上次存的 Last-Modified,即 “Wed, 23 Dec 2025 10:00:00 GMT”;另一个是 If-None-Match,值就是上次存的 ETag,即 “5f8d02a7-1234”。
第三步,服务器收到这个请求,首先会检查 If-None-Match 和当前接口数据的 ETag 是否一致。因为 ETag 是哈希值,能精确判断数据有没有变化,所以优先级比 Last-Modified 高。如果两个 ETag 一致,说明数据没改,服务器直接返回 304 Not Modified,响应体里没有数据,只带一些基本的响应头。
第四步,如果 ETag 不一致,服务器再去对比 If-Modified-Since 和当前数据的最后修改时间。如果时间一致,还是返回 304;如果时间不一致,说明数据真的更新了,服务器就返回 200 OK,响应体里是新的数据,同时在响应头里更新 Last-Modified 和 ETag 的值,浏览器会用新数据覆盖旧缓存。
最后,浏览器收到响应,如果是 304,就从本地缓存里取出之前存的数据来用;如果是 200,就用新数据,并更新缓存里的标识。
代理缓存
源服务器来说,它也是有缓存的,比如Redis, Memcache
TCP协议和UDP协议与TCP/IP协议的区别

TCP/IP 中有两个具有代表性的传输层协议,分别是 TCP 和 UDP
TCP/IP 是互联网相关的各类协议族的总称,比如:TCP,UDP,IP,FTP,HTTP,ICMP,SMTP 等都属于 TCP/IP 族内的协议。
TCP/IP协议集包括应用层,传输层,网络层,网络访问层。

-
链路层:负责封装和解封装IP报文,发送和接受ARP/RARP报文等。
-
网络层:负责路由以及把分组报文发送给目标网络或主机。
-
传输层:负责对报文进行分组和重组,并以TCP或UDP协议格式封装报文。
-
应用层:负责向用户提供应用程序,比如
HTTP、FTP、Telnet、DNS、SMTP等。
TCP和UDP区别
TCP建立连接要进行3次握手,而断开连接要进行4次
UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。
总结:
1、TCP 是面向连接的,udp 是无连接的即发送数据前不需要先建立链接
2、对系统资源的要求(TCP较多,UDP少);
3、UDP程序结构较简单;
4、TCP 是面向字节流,UDP 面向报文, ;
5、TCP保证数据正确性可靠的,UDP可能丢包;
6、TCP保证数据顺序,UDP不保证。
7、Tcp仅支持单播传输,udp有单播,多播,广播的功能
http跨域问题
原因:同源策略
同源策略:就是浏览器的一个安全机制,不同源的客户端脚本没有在明确授权的情况下,不能读写对方资源
请求跨域了,那么请求到底发出去没有?
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了
解决跨域:
JSONP()
-
jsonp只可以使用GET方式提交 -
不好调试,在调用失败的时候不会返回任何状态码
-
不安全可能会遭受XSS攻击。,万一假如提供
jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。那么结果是什么?所有调用这个jsonp的网站都会存在漏洞。于是无法把危险控制在一个域名下…所以在使用jsonp的时候必须要保证使用的jsonp服务必须是安全可信的。
JSONP和AJAX对比
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
JSONP原理
利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据
// index.html
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback } // wd=b&callback=show
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log(data)
})
jQuery的jsonp形式
JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。
$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});
CORS
"跨域资源共享"(Cross-origin resource sharing)
他允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服啦 AJAX 只能同源使用的限制
浏览器将CORS请求分成两类:简单请求和非简单请求
简单请求
只要同时满足以下两大条件,就属于简单请求
条件1:使用下列方法之一:
- GET
- HEAD
- POST
条件2:Content-Type 的值仅限于下列三者之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
复杂请求
复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求
我们用PUT向后台请求时,属于复杂请求,后台需做如下配置:
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段
// 前端代码
var url = 'http://localhost:2333/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//得到响应头,后台需设置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()
复制代码
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
复制代码
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'jw') //返回一个响应头,后台需设置
res.end('我不爱你')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)
nginx反向代理、
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
先下载nginx,然后将nginx目录下的nginx.conf修改如下:
// proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
复制代码
最后通过命令行nginx -s reload启动nginx
// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
复制代码
// server.js
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...')
总结
- CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
- JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
- 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
- 日常工作中,用得比较多的跨域方案是cors和nginx反向代理
浏览器渲染机制(CRP过程)
juejin.im/post/684790…
1:浏览器将获取的HTML文档解析成DOM树(
在这步中,浏览器从开始解析开始就会启用另外一个线程来下载其他的css,js,静态资源等文件,但是如果遇到script和css文件)标签就会停下来,等待加载完成载继续解析,这也是为什么提倡把script标签放到页面底部、不适用import的方式导入css的文件原因。)
2:生成CSSOM树:CSS下载完成后对CSS文件进行解析,解析成CSS对象,然后对CSS对象进行组装 ,生成cssom树
3:构建一个渲染树 当DOM树和CSSOM树都构建完成后,浏览器根据DOM树和CSSOM树,构建一个渲染树:(rendering tree) 代表一系列即将被渲染的对象
4:布局 浏览器用一种流式处理的办法对渲染树上的每个节点,计算其在屏幕上的位置,这一步称之为布局(layout)
5:绘制:遍历渲染树,将其绘制到屏幕上。这一步称之为绘制
个人简单总结
1:浏览器获取html文档解析 DOM树
2:生成CSSOM树:在上面一步把css下载网完对css进行解析成css对象,css对象进行组装生成CSSOM树
3:构建渲染树:当DOM树和CSSOM树构建后,浏览器根据DOM和CSSOM树构建一个渲染对象
4:布局:浏览器用流式方法对渲染树上每个节点,计算它在屏幕的位置
5:绘制:遍历渲染树,绘制到屏幕上
老师总结:
浏览器的渲染机制一般分为以下几个步骤
处理 HTML 并构建 DOM 树。
处理 CSS 构建 CSSOM 树。
将 DOM 与 CSSOM 合并成一个渲染树。
根据渲染树来布局,计算每个节点的位置。
调用 GPU 绘制,合成图层,显示在屏幕上。
这个过程不是一次进行的,而是存在一定的交叉,后面会详细解析
Dom解析规则
总结:
-
CSS不会阻塞DOM的解析,但会阻塞DOM渲染。 -
JS阻塞DOM解析,但浏览器会"偷看"DOM,预先下载相关资源。 -
浏览器遇到
<script>且没有defer或async属性的 标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。
2. CSS 对 DOM 树生成的影响
CSS 的加载和执行可能会阻塞 DOM 树的生成和渲染,具体表现如下:
2.1 CSS 阻塞 DOM 渲染
- 当浏览器遇到
<link>标签引入的外部 CSS 文件时,它会立即开始下载 CSS 文件。 - 在 CSS 文件下载并解析完成之前,浏览器会阻塞页面的渲染(即不会显示任何内容),以避免 FOUC(Flash of Unstyled Content) 问题。
- 这种阻塞行为是为了确保页面在渲染时已经应用了正确的样式。
2.2 CSS 阻塞 JavaScript 执行
- 如果 JavaScript 试图访问某些元素的样式属性,浏览器需要确保相关的 CSS 已经加载并解析完成。
- 因此,CSS 文件的加载和执行会阻塞 JavaScript 的执行,进而间接阻塞 DOM 树的生成。
3. JavaScript 对 DOM 树生成的影响
JavaScript 的加载和执行会直接影响 DOM 树的生成,具体表现如下:
3.1 JavaScript 阻塞 DOM 解析
- 当浏览器遇到
<script>标签时,它会立即停止 DOM 树的生成,开始下载并执行 JavaScript 文件。 - 这是因为 JavaScript 可能会修改 DOM 结构(例如通过
document.write或appendChild),浏览器需要确保 DOM 树的正确性。 - 这种阻塞行为会导致页面的渲染延迟。
3.2 异步和延迟加载 JavaScript
为了减少 JavaScript 对 DOM 树生成的阻塞,可以使用以下方法:
async属性:异步加载 JavaScript 文件,下载完成后立即执行,不阻塞 DOM 解析。defer属性:延迟加载 JavaScript 文件,在 DOM 解析完成后按顺序执行。- 将
<script>标签放在<body>底部:确保 DOM 解析完成后再加载 JavaScript。
4. CSS 和 JavaScript 的加载顺序
CSS 和 JavaScript 的加载顺序会影响页面的渲染性能,具体表现如下:
4.1 CSS 在 JavaScript 之前加载
- 如果 CSS 文件在 JavaScript 文件之前加载,浏览器会先下载并解析 CSS 文件,然后再下载并执行 JavaScript 文件。
- 这种顺序可以确保 JavaScript 执行时,CSS 已经准备就绪,避免样式计算错误。
4.2 JavaScript 在 CSS 之前加载
- 如果 JavaScript 文件在 CSS 文件之前加载,JavaScript 的执行可能会被 CSS 文件的加载阻塞。
- 这种顺序会导致页面渲染的延迟。
5. 优化策略
为了减少 CSS 和 JavaScript 对 DOM 树生成的阻塞,可以采取以下优化策略:
5.1 优化 CSS 加载
- 将
<link>标签放在<head>中:确保浏览器尽早开始下载 CSS 文件。 - 使用
media属性:为不同设备加载不同的 CSS 文件,减少不必要的下载。 - 内联关键 CSS:将首屏渲染所需的关键 CSS 直接内联到 HTML 中,减少外部 CSS 文件的阻塞。
5.2 优化 JavaScript 加载
- 使用
async或defer属性:避免 JavaScript 阻塞 DOM 解析。 - 将
<script>标签放在<body>底部:确保 DOM 解析完成后再加载 JavaScript。 - 按需加载 JavaScript:使用动态导入(
import())或懒加载技术,减少初始加载的 JavaScript 文件大小。
5.3 使用预加载
<link rel="preload">:提前加载关键资源(如 CSS、JavaScript),减少后续阻塞时间。
5.4 减少渲染阻塞资源
- 压缩和合并 CSS、JavaScript 文件:减少文件数量和大小,加快下载速度。
- 使用 CDN:加快资源加载速度。
回流(reflow)与重绘(repaint)
a. 回流(reflow):
就是页面的布局发生了变化,浏览器需要重新的计算各个节点的位置,大小等信息了, 浏览器会从root frame递归向下计算。这个回去重新计算的过程就是回流。
b. 重绘(repaint):
是指一个元素的外观被改变了,背景颜色、文字颜色、边框颜色等。就会引起浏览器对某一部分的重画, 但是并不会引起页面布局的改变。( 另外有些情况下,比如修改了元素的样式, 浏览器并不会立刻reflow或repaint一次, 而是会把这样的操作积攒一批,然后做一次reflow, 这又叫异步reflow或增量异步reflow。但是在有些情况下, 比如resize窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行reflow。)
个人总结:回流必定会引起重绘,但是重绘不一定会引起回流。
重绘:就是指某个元素的颜色,边框等样式改版,引起浏览器的重新绘制,但是不会引起页面布局改变
回流:就是页面布局发生改变,浏览器需要重新计算各个节点位置,渲染树遍历,绘制到屏幕上
重绘(repaint): 当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此 损耗较少
回流(reflow): 当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。会触发回流的操作:
页面初次渲染
浏览器窗口大小改变
元素尺寸、位置、内容发生改变
元素字体大小变化
添加或者删除可见的 dom 元素
激活 CSS 伪类(例如::hover)
查询某些属性或调用某些方法
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
getComputedStyle()
getBoundingClientRect()
scrollTo()
回流必定触发重绘,重绘不一定触发回流。重绘的开销较小,回流的代价较高。
避免回流和重绘
- 通过优化 CSS、减少 DOM 操作、使用
transform和opacity等方法,可以有效减少重绘和重排,提升页面性能。
分析:
使用 transform 和 opacity 来实现动画,因为它们不会触发重排和重绘,而是由 GPU 加速渲染。
避免使用 top、left 等属性来实现动画,因为它们会触发重排
前端如何进行seo优化
合理的title、description、keywords:搜索对着三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。 语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页 重要内容HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取 重要内容不要用js输出:爬虫不会执行js获取内容 少用iframe:搜索引擎不会抓取iframe中的内容 非装饰性图片必须加alt 提高网站速度:网站速度是搜索引擎排序的一个重要指标
总结:
1:网页优化3剑客,头部title、meta标签name属性的description、meta标签name属性的keywords

2:小技巧网址logo

3:使用语义化元素
如:
<h1><em> 或 <strong>
4:利用 <img> 中的 alt 属性
5:扁平化网站结构
排版结构越小
6:少用iframe:搜索引擎不会抓取iframe中的内容
1000-div问题
主要用到两个方法 document.createDocumentFragment() / window.requestAnimationFrame()
这个documentFragment对象不属于文档树,继续的parentNode属性总是null, 把dom文档树插入它时候,不是插入documentFragment自身,而是插入的它的所有子孙节点, 它相当于一个占位符,暂时存放插入的文档节点,方便文档的复制、剪切, 要添加多个dom元素时候,将这些元素放到DocuemntFragment中,统一添加页面上,这样减少dom的次数渲染
requestAnimationFrame和setTimeout/setInterVal的优点
-
requestAnimationFrame把每一帧的所有Dom操作集中起来,一次完成重绘和回流、并且这个重绘和回流时间跟 随浏览器刷新频率间隔紧,一般来说,这个频率每秒60帧
-
隐藏的元素中,requestAnimationFrame 将不会进行的重绘和回流,这样减少渲染消耗的性能 `
<ul></ul>
createDocumentFragmentFun(){
const total=10000000; // 总插入的数量
const once=20;// 每次插入条数
const loopCount=Math.ceil(total/once); // 需要插入的总次数
let countRender=0;// 渲染的次数
const ul =document.querySelector('ul');
// 添加数据的方法
function add(){
// 创建虚拟节点对象
const fragment=document.createDocumentFragment()
for(let i=0; i<once;i++){
const li=document.createElement('li');
li.innerHTML=Math.floor(Math.random()*100000);
fragment.appendChild(li);
}
ul.appendChild(fragment);
countRender++;
loop()
}
function loop(){
if(loopCount>countRender){
window.requestAnimationFrame(add) // 每次执行方法
}
}
loop()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<ul>
控件
</ul>
<script>
setTimeout(() => {
// 插入十万条数据
const total = 100000
// 一次插入 20 条,如果觉得性能不好就减少
const once = 20
// 渲染数据总共需要几次
const loopCount = total / once
let countOfRender = 0
let ul = document.querySelector('ul')
function add() {
// 优化性能,插入不会造成回流
const fragment = document.createDocumentFragment()
for (let i = 0; i < once; i++) {
const li = document.createElement('li')
li.innerText = Math.floor(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
countOfRender += 1
loop()
}
function loop() {
if (countOfRender < loopCount) {
window.requestAnimationFrame(add)
}
}
loop()
}, 0)
</script>
</body>
</html>
注意:document.createDocuemntFragment 和 requestAnimationFrame 的frame和fragment单词不一样
http和浏览器详细 参考链接
-------------------ES6------------------------------------------
Promise 的研究
一 为什么要使用Promise
由于异步任务不能直接拿到结果,于是我们传一个回调(函数)给异步任务,当异步任务完成时调用回调,同时调用的时候把异步任务的结果作为回调的参数。但是该方法容易出现回调地狱,代码变得使人看不懂,因此使用Promise。
基本用法 Promise.then
var p=new Promise((reslove,reject)=>{
setTimeout(()=>{
if('成立'){
reslove('成立')
}else{
reject('不成立')
}
},1000)
})
p.then((res)=>{
console.log(res) //成立
})
第二种:
new Promise((resolve,reject)=>{
resolve(1)
console.log(2)
}).then((res)=>{
console.log(res) // 2 1
})
方法一:

方法二:

Promise.catch()

new Promise((resolve,reject)=>{
resolve(1 + x);
}).then(res=>{
console.log('res',res)
}).catch(error=>{
console.log("error",error)
})
//ReferenceError: x is not defined
// at porm.html:17
// at new Promise (<anonymous>)
// at porm.html:16
Promise.all
1:所有的prmise成功
let promise1 = new Promise(resolve => {
resolve(1);
})
let promise2 = new Promise(resolve => {
resolve(2);
})
let promises = [promise1, promise2];
Promise.all(promises).then(res => {
console.log("promise3");
console.log('res',res);
}).catch(err => {
console.log("promise all err:", err); //
})
// promise3 [1,2]
2:其中一个promise失败 :直接抛出失败
let promise1 = new Promise(resolve => {
resolve(1);
})
let promise2 = new Promise(resolve => {
resolve(2);
})
let promise3 = new Promise(resolve => {
resolve(x + 3);
}).then((value) => {
console.log(value);
})
let promises = [promise1, promise2,promise3];
Promise.all(promises).then(res => {
console.log("promise3");
console.log('res',res);
}).catch(err => {
console.log("promise all err:", err); //
})
// x is not defined
3:all.的then和catch同时使用
let promise1 = new Promise(resolve => {
console.log("promise1");
resolve(1);
})
let promise2 = new Promise(resolve => {
console.log("promise2");
resolve(x+2);
})
let promises = [promise1, promise2];
Promise.all(promises).then(res => {
console.log("promise3");
console.log('res',res);
}).catch(err => {
console.log("promise all err:", err); //
})
// promise1
// promise2
// promise all err: ReferenceError: x is not defined
Prmise.race 返回最先 resolve 值
let promise1 = new Promise(resolve => {
console.log("promise1");
resolve(1);
})
let promise2 = new Promise(resolve => {
console.log("promise2");
resolve(2);
})
let promise3 = new Promise(resolve => {
console.log("promise3");
resolve(3);
})
let promises = [promise1, promise2, promise3];
Promise.race(promises).then(value => {
console.log(value);
}).catch(err => {
console.log(err);
})
结果:
// promise1
// promise2
// promise3
// 1
let promise1 = new Promise(resolve => {
console.log("promise1");
setTimeout(()=>{
resolve(1);
},3000)
})
let promise2 = new Promise(resolve => {
console.log("promise2");
setTimeout(()=>{
resolve(1);
},2000)
})
let promise3 = new Promise(resolve => {
console.log("promise3");
setTimeout(()=>{
resolve(3);
},1000)
})
let promises = [promise1, promise2, promise3];
Promise.race(promises).then(value => {
console.log(value);
}).catch(err => {
console.log(err);
})
//// promise1
// promise2
// promise3
// 3
let promise1 = new Promise(resolve => {
console.log("promise1");
resolve(1);
})
let promise2 = new Promise(resolve => {
console.log("promise2");
resolve(2);
})
let promise3 = new Promise(resolve => {
console.log("promise3");
resolve(3);
})
let promises = [promise1, promise2, promise3];
Promise.all(promises).then(value => {
console.log(value);
}).catch(err => {
console.log(err);
})
// promise1
// promise2
// promise3
// [1,2,3]
Promise.resolve
Promise.resolve的作用就是将一个对象转换成Promise对象。
let thenable = {
then:function(resolve,reject){
resolve(12);
}
}
let p = Promise.resolve(thenable);
p.then((value)=>{
console.log(value); // 12
})
Promise.resolve方法会将这个对象转换成Promise对象,然后立即执行thenable对象的then方法。
所有上面的代码,结果会输出12。
var p = Promise.resolve("Hello");
p.then((s)=>{
console.log(s) //Hello
})
setTimeout(() => {
console.log("xxxx");
}, 0);
Promise.resolve().then(() => {
console.log("yyyyy");
})
console.log("zzzz");
// zzzz
/// yyyyy
// xxxx
Promise.finally
finally方法用于指定不管Promise对象最后状态如何都会执行的操作。
let p2 = new Promise((resolve, reject) => {
resolve(1);
})
p2.then((value) => {
console.log(value);
}).catch((err) => {
console.log("error:", err);
}).finally(() => {
console.log("finally");
})
//1 finally
Promise.any() :
- 接收一个
Promise数组,返回第一个成功的Promise的结果。如果所有Promise都失败,则返回一个AggregateError。
const promises = [
Promise.reject("Error 1"),
Promise.resolve("Success!"),
Promise.reject("Error 2"),
];
Promise.any(promises)
.then((result) => console.log(result)) // 输出 "Success!"
.catch((error) => console.error(error));
race和all的区别

Promise.reject()
- 返回一个已经失败(rejected)的
Promise对象。
Promise.reject("Rejected!").catch((error) => console.error(error)); // 输出 "Rejected!"
Promise.allSettled() :
- 接收一个
Promise数组,等待所有Promise完成(无论成功或失败),并返回一个包含每个Promise结果的对象数组。
const promises = [
Promise.resolve(1),
Promise.reject("Error"),
Promise.resolve(3),
];
Promise.allSettled(promises).then((results) => console.log(results));
// 输出:
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'Error' },
// { status: 'fulfilled', value: 3 }
// ]
promise有三种状态
- pending
- resolve
- rejected
方法总结
| 方法类型 | 方法名 | 描述 |
|---|---|---|
| 实例方法 | then() | 添加成功或失败的回调函数 |
| 实例方法 | catch() | 捕获失败状态 |
| 实例方法 | finally() | 无论成功或失败都会执行的回调函数 |
| 静态方法 | Promise.resolve() | 返回一个成功的 Promise |
| 静态方法 | Promise.reject() | 返回一个失败的 Promise |
| 静态方法 | Promise.all() | 等待所有 Promise 成功,返回一个包含所有结果的数组 或第一个失败 |
| 静态方法 | Promise.allSettled() | 等待所有 Promise 完成(无论成功或失败) |
| 静态方法 | Promise.race() | 返回第一个完成的 Promise(无论成功或失败) |
| 静态方法 | Promise.any() | 返回第一个成功的 Promise,如果全部失败则返回 AggregateError |
async/await和promise的区别:
- async/await 是建立在 Promises上的,不能被使用在普通回调以及节点回调
- async/await相对于promise来讲,写法更加优雅
reject和catch区别
一、当Promise.then中有第二个回调函数时,执行then第二个函数。
let p=new Promise((resolve,reject)=>{
throw new Error('出错了')
})
p.then(()=>{},err=>{
console.log('reject'+err);
}).catch(err=>{
console.log('catch'+err);
})
结果:rejectError: 出错了复制代码
二、当Promise.then中没有第二个回调函数时,执行catch。
let p=new Promise((resolve,reject)=>{
throw new Error('出错了')
})
p.then(()=>{}).catch(err=>{
console.log('catch'+err);
})
结果:catchError: 出错了复制代码
三、网络异常,会直接进入catch而不会进入then的第二个回调
虚拟Dom的了解
虚拟 DOM(Virtual DOM)本质上是 JS 和 DOM 之间的一个映射缓存,它在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象
字符串的slice ,substr ,和substring区别
let numstr="0123456789十"
console.log('slice(3)',numstr.slice(3))//3456789十 0~3下标的字符串片段;
console.log('slice(3,-1)',numstr.slice(3,-1))//3456789
console.log('substr(3,5)',numstr.substr(3,5))//34567 5是个数
console.log('substring(3,5)',numstr.substring(3,5))//34
总结:都是截取字符串片段,参数不一样
1:slice 和 substring 区别 slice可以接受负数参数,而 substring不行,
2:subStr 第二个参数是截取的长度
3: slice 和 substring 参数是截止下标位置前(但是不包括这个下标)
slice和splice 、pop,shift方法区别
let array1=[0,1,2,3,4,5,6,7,8,9]
console.log('后面添加push() 改变原数组',array1.push(10),array1)//后面添加[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log('前面添加unshift() 改变原数组',array1.unshift(10),array1)//前面添加[10,0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log('删除最后一个pop() 改变原数组',array1.pop(),array1)//删除最后一个[10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log('删除第一个shift() 改变原数组',array1.shift(),array1)//删除第一个 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log('splice(0,1,删除后添加) 改变原数组',array1.splice(0,1,'删除后添加'),array1) //['删除后添加', 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log('splice(0,1) 改变原数组',array1.splice(0,1),array1) // 返回:['删除后添加'], 原来:[1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log('splice(2) 改变原数组',array1.splice(2),array1) ////返回:[3, 4, 5, 6, 7, 8, 9], 原来:[1,2]
array1.push(3,4,5,6,7,8,9)
array1.unshift(0)
console.log('slice(1,3) 没有改变原数组',array1.slice(1,3),array1)// 返回:[1, 2] 原来数组:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log('slice(1,-3) 没有改变原数组',array1.slice(1,-3),array1)// 返回:[1, 2, 3, 4, 5, 6] 原来数组:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log('slice(1) 没有改变原数组',array1.slice(1),array1)// 返回:1, 2, 3, 4, 5, 6, 7, 8, 9] 原来数组:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
总结:
1:slice 截取数组返回新的数组,没有改变原数组,splice/pop改变原数组
2:splice 第三个参数,就是删除元素位置,替换的字符
splice 一个参数时候可以出现负数,两参数不能出现负数
3:slice可以1~2 个参数,pop没有两个
4: slice 第二个参数是下标位置, splice 是元素个数

ajax的优点和缺点
优点
1:局部刷新,减少用户等待请求的时间,提高交互 2:减轻服务器的压力 3:xml数据标准间,促进数据与页面分离
缺点
1:只局部刷新,前后后又清新请求 2:大量使用引擎,有一定兼容性
js清除浏览器缓存的几种办法
一、meta方法
//不缓存
<META HTTP-EQUIV="pragma" CONTENT="no-cache">
<META HTTP-EQUIV="Cache-Control" CONTENT="no-cache, must-revalidate"> <META HTTP-EQUIV="expires" CONTENT="0">
二、清理form表单的临时缓存
<body onLoad="javascript:document.yourFormName.reset()">\
三、 jquery ajax清除浏览器缓存
- 方式一:用ajax请求服务器最新文件,并加上请求头If-Modified-Since和Cache-Control,如下:
$.ajax({
url:'www.haorooms.com',
dataType:'json',
data:{},
beforeSend :function(xmlHttp){
xmlHttp.setRequestHeader("If-Modified-Since","0");
xmlHttp.setRequestHeader("Cache-Control","no-cache");
},
success:function(response){
//操作
}
async:false
});
- 方法二,直接用cache:false,
$.ajax({
url:'www.haorooms.com',
dataType:'json',
data:{},
cache:false,
ifModified :true ,
success:function(response){
//操作
}
async:false
});
- 方法三:用随机数,随机数也是避免缓存的一种很不错的方法!
URL 参数后加上 "?ran=" + Math.random(); //当然这里参数 ran可以任意取了\
- 方法四:用随机时间,和随机数一样。
在 URL 参数后加上 "?timestamp=" + new Date().getTime();\
数据类型检测的方式有哪些
typeof
console.log(typeof 123); // number
instanceof
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
Object.prototype.toString
console.log(Object.prototype.toString.call(42)); // "[object Number]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call(function() {})); // "[object Function]"
typeof与instanceof区别
typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值instanceof可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
判断数组的方式有哪些
Array.isArray() es6方法做判断
Array.isArrray(obj);
instanceof做判断
var obj=new Array();
obj instanceof Array
Object.prototype.toString
-
用
Object.prototype.toString方法,检查返回值是否为[object Array]。const arr = [1, 2, 3]; console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true -
优点:
- 兼容性好,适用于所有环境。
- 可以准确区分数组和其他对象。
constructor
-
检查变量的
constructor属性是否为Array。const arr = [1, 2, 3]; console.log(arr.constructor === Array); // true -
注意:
- 如果变量的
constructor被修改,可能会导致错误判断。 - 不推荐在不可控的环境中使用。
- 如果变量的
判断一个变量是否是对象,有哪些办法?
typeOf
function isObject(value) {
return value !== null && typeof value === 'object';
}
instanceof
instanceof可以检查一个对象是否是某个构造函数的实例。
function isObject(value) {
return value instanceof Object;
}
- 这种方法会返回
true对于数组、函数、日期等,因为它们都是Object的实例。 - 对于原始类型(如
null和undefined),会返回false。
constructor
- 检查变量的
constructor属性是否为Object。
function isObject(value) {
return value && value.constructor === Object;
}
-
注意:
- 这种方法只适用于普通对象,不适用于数组、函数等。
Object.prototype.toString
-
调用
Object.prototype.toString方法可以精确判断对象的类型。function isObject(value) { return Object.prototype.toString.call(value) === '[object Object]'; } -
注意:
- 这种方法可以准确区分普通对象、数组、函数等。
- 对于自定义对象或内置对象(如
Date、RegExp),返回的字符串会不同。
Object.getPrototypeOf
-
通过获取对象的原型链来判断是否是普通对象。
function isObject(value) { return Object.getPrototypeOf(value) === Object.prototype; } -
注意:
- 这种方法适用于普通对象,但不适用于数组、函数等。
判断属性是否属于该对象的成员
1. in 运算符
- 作用: 检查属性是否在对象或其原型链中。
- 语法:
propertyName in object - 返回值:
true(属性存在)或false(属性不存在)。
示例:
const obj = { name: 'Alice', age: 25 };
console.log('name' in obj); // true
console.log('toString' in obj); // true(toString 是原型链上的属性)
console.log('gender' in obj); // false
2. Object.prototype.hasOwnProperty()
- 作用: 检查属性是否是对象自身的属性(不包括原型链)。
- 语法:
object.hasOwnProperty(propertyName) - 返回值:
true(属性是对象自身的)或false(属性不存在或是原型链上的)。
示例:
const obj = { name: 'Alice', age: 25 };
console.log(obj.hasOwnProperty('name')); // true
console.log(obj.hasOwnProperty('toString')); // false(toString 是原型链上的属性)
console.log(obj.hasOwnProperty('gender')); // false
hasOwnProperty 检测一个属性是否是对象的自有属性
var f = new F(); //实例化对象
console.log(f.hasOwnProperty("name")); //返回true,说明当前调用的 name是自有属性
console.log(f.name); //返回字符串“自有属性
hasOwnProperty和Object.prototype.hasOwnProperty() 区别
- 调用方式:
hasOwnProperty是对象实例的方法,而Object.prototype.hasOwnProperty()是原型上的方法,需要通过call或apply来调用。 - 安全性: 如果对象重写了
hasOwnProperty方法,直接调用obj.hasOwnProperty可能会导致意外行为。使用Object.prototype.hasOwnProperty.call(obj, prop)可以避免这个问题,因为它直接调用原型上的方法。
3. Object.hasOwn() (ES2022 新增)
- 作用: 与
hasOwnProperty类似,但更安全(避免对象重写hasOwnProperty方法的问题)。 - 语法:
Object.hasOwn(object, propertyName) - 返回值:
true(属性是对象自身的)或false(属性不存在或是原型链上的)。
示例:
const obj = { name: 'Alice', age: 25 };
console.log(Object.hasOwn(obj, 'name')); // true
console.log(Object.hasOwn(obj, 'toString')); // false
console.log(Object.hasOwn(obj, 'gender')); // false
4. Reflect.has()
- 作用: 检查属性是否在对象或其原型链中(类似于
in运算符)。 - 语法:
Reflect.has(object, propertyName) - 返回值:
true(属性存在)或false(属性不存在)。
示例:
const obj = { name: 'Alice', age: 25 };
console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'toString')); // true
console.log(Reflect.has(obj, 'gender')); // false
对比总结
| 方法 | 检查范围 | 是否包括原型链 | 是否安全(避免重写问题) |
|---|---|---|---|
in 运算符 | 对象及其原型链 | 是 | 是 |
hasOwnProperty() | 对象自身属性 | 否 | 否(可能被重写) |
Object.hasOwn() (ES2022) | 对象自身属性 | 否 | 是 |
Reflect.has() | 对象及其原型链 | 是 | 是 |
推荐使用
- 如果需要检查对象自身属性,推荐使用
Object.hasOwn()(ES2022 新增,更安全)。 - 如果需要检查对象及其原型链上的属性,可以使用
in运算符 或Reflect.has()。
Object.is()
判断两个值是否先等
object和== 、===区别
==:等同,比较运算符,两边值类型不同的时候,先进行类型转换,再比较;
===:恒等,严格比较运算符,不做类型转换,类型不同就是不等;
Object.is()是ES6新增的用来比较两个值是否严格相等的方法,与===的行为基本一致。
其行为与`===`基本一致,不过有两处不同:
Object.is('cesi','cesi') // true
**+0**不等于**-0**。
**NaN**等于自身
创建一个空数组/空对象有哪些方式
// 创建空数组
const arr1 = [];
const arr2 = new Array();
const arr3 = Array.from({ length: 0 });
const arr4 = Array.of();
// 创建空对象
const obj1 = {};
const obj2 = new Object();
const obj3 = Object.create(null);
const obj4 = Object.assign({});
console.log(arr1, arr2, arr3, arr4); // [] [] [] []
console.log(obj1, obj2, obj3, obj4); // {} {} {} {}
什么是尾调用,使用尾调用有什么好处?
要满足尾调用优化,必须满足以下条件:
- 函数的最后一步是一个函数调用。
- 调用后不需要执行任何其他操作(例如计算、返回等)。
示例:
// 尾调用
function foo() {
return bar();
}
// 不是尾调用
function foo() {
return bar() + 1; // 调用后还需要执行加法操作
}
// 不是尾调用
function foo() {
bar(); // 调用后还需要执行隐式的 return undefined
}
总结
- 尾调用是指函数的最后一步是调用另一个函数,并且调用后没有其他操作。
- 尾调用优化可以节省内存、避免栈溢出并提高性能。
- 尾调用优化需要满足特定条件,但目前大多数 JavaScript 引擎并未完全支持。
如果需要处理大量递归调用,可以考虑使用尾递归或手动优化(例如使用循环替代递归
commonJs和ES6 Module
- CommonJS是对模块的浅拷⻉
- ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const
ES6模块与CommonJS模块有什么异同?
ES6 Module和CommonJS模块的共同点:
CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。
// 获取父元素 var container = document.getElementById('container') // 获取两个需要被交换的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交换两个元素,把 content 置于 title 前面 container.insertBefore(content, title)
use strict是什么意思 ? 使用它区别是什么?
严格运行模式 设立严格模式的目的如下:
- 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;
- 消除代码运行的不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的 Javascript 做好铺垫。
区别:
- 禁止使用 with 语句。
- 禁止 this 关键字指向全局对象。
- 对象不能有重名的属性。
for, forEach,for in,for of, map 区别 JS循环大总结
1:for【对象,数组】
2:forEach【数组】
- 便利的时候更加简洁,效率和for循环相同,不用关心集合下标的问题,减少了出错的概率。
- 没有返回值
- 不能使用break中断循环,不能使用return返回到外层函数
注意:
- forEach() 对于空数组是不会执行回调函数的。
- for可以用continue跳过循环中的一个迭代,forEach用continue会报错。
- forEach() 需要用 return 跳过循环中的一个迭代,跳过之后会执行下一个迭代。
let newarr=arr.forEach(i=>{
i+=1;
console.log(i);//2,4,5
})
console.log(arr)//[1,3,4]
console.log(newarr)//undefined
var arry=[1,4,5,6,7,8,9]
arry.forEach(item=>{
if(item<3) return
console.log(item)
})
//4
//5
//6
//7
//8
//9
3:for in【大部分用于对象】
用于循环遍历数组或对象属性,for in更适合遍历对象,当然也可以遍历数组,但是会存在一些问题
let person={name:"小白",age:28,city:"北京"}
let text=""
for (let i in person){
text+=person[i]
}
输出结果为:小白28北京
//其次在尝试一些数组
let arry=[1,2,3,4,5]
for (let i in arry){
console.log(arry[i])
}
//能输出出来,证明也是可以的
注意
for in更适合遍历对象,当然也可以遍历数组,但是会存在一些问题,
比如:index索引为字符串型数字,不能直接进行几何运算
var arr = [1,2,3]
for (let index in arr) {
let res = index + 1
console.log(res)
}
//01 11 21
4:for of【Es6 新增方法 不能遍历普通对象(要转换Interratro接口数据类型)】
- (可遍历map,object,array,set string等)用来遍历数据,比如组中的值
- 避免了for in的所有缺点,可以使用break,continue和return,不仅支持数组的遍历,还可以遍历类似数组的对象。 for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。
for (let item of arr){ console.log(item) }
5:map【数组方法】
- map不改变原数组但是会 返回新数组
- 可以使用break中断循环,可以使用return返回到外层函数
var arry=[1,3,4]
let newarr=arr.map(i=>{
return i+=1;
console.log(i);
})
console.log(arr)//1,3,4---不会改变原数组
console.log(newarr)//[2,4,5]---返回新数组
总结:
- forEach 遍历列表值,不能使用 break 语句或使用 return 语句
- for in 遍历对象键值(key),或者数组下标,不推荐循环一个数组
- for of 遍历列表值,允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等.在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。
- for in循环出的是key,for of循环出的是value;
- for of是ES6新引入的特性。修复了ES5的for in的不足;
- for of不能循环普通的对象,需要通过和Object.keys()搭配使用。
数组的遍历方法有哪些
| 方法 | 是否改变原数组 | 特点 |
|---|---|---|
| forEach() | 否 | 数组方法,不改变原数组,没有返回值 |
| map() | 否 | 数组方法,不改变原数组,有返回值,可链式调用 |
| filter() | 否 | 数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用 |
| for...of | 否 | for...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环 |
| every() 和 some() | 否 | 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false. |
| find() 和 findIndex() | 否 | 数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值 |
| reduce() 和 reduceRight() | 否 | 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作 |
具体写法:
forEach和map方法有什么区别
这方法都是用来遍历数组的,两者区别如下:
- forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
- map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;
ajax、axios、fetch的区别
ajax
- 本身是针对MVC编程,不符合前端MVVM的浪潮
- 基于原生XHR开发,XHR本身的架构不清晰
- 不符合关注分离(Separation of Concerns)的原则
- 配置和调用方式非常混乱,而且基于事件的异步模型不友好。
fetch
fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。 fetch的优点:
- 语法简洁,更加语义化
- 基于标准 Promise 实现,支持 async/await
- 更加底层,提供的API丰富(request, response)
- 脱离了XHR,是ES规范里新的实现方式
fetch的缺点:
- fetch只对网络请求报错,对400,500都
当做成功的请求 ,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
- fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
- fetch没有办法原生监测请求的进度,而XHR可以
axios
- 浏览器端发起XMLHttpRequests请求
- node端发起http请求
- 支持Promise API
- 监听请求和返回
- 对请求和返回进行转化
- 取消请求
- 自动转换json数据
- 客户端支持抵御XSRF攻击
宏任务和微任务
定义:是js处理异步操作的机制,事件循环(Event Loop)中的执行顺序不同
微任务(Microtasks)
- 定义: 需要尽快执行的异步任务。
- 常见类型:
Promise的回调(then、catch、finally)MutationObserver的回调process.nextTick(Node.js环境)
- 执行时机: 在当前任务结束后、下一个宏任务开始前执行。
- 队列: 按先进先出(FIFO)顺序执行。
宏任务(Macrotasks)
- 定义: 可以稍后执行的异步任务。
- 常见类型:
setTimeoutsetIntervalsetImmediate(Node.js环境)requestAnimationFrame- I/O操作
- UI渲染
- 执行时机: 在下一个事件循环中执行。
- 队列: 按先进先出(FIFO)顺序执行。
事件循环中的执行顺序
- 同步代码: 首先执行所有同步代码。
- 微任务: 执行所有微任务。
- 宏任务: 执行一个宏任务。
- 重复: 重复上述过程,直到所有任务完成。
示例代码
console.log('Start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
输出顺序
StartEndPromisesetTimeout
执行流程
- 同步代码: 输出
Start和End。 - 微任务: 执行
Promise回调,输出Promise。 - 宏任务: 执行
setTimeout回调,输出setTimeout。
总结
- 微任务: 高优先级,当前任务结束后立即执行。
- 宏任务: 低优先级,下一个事件循环中执行。
- 执行顺序: 同步代码 > 微任务 > 宏任务。

经典示例分析
console.log('script start'); // 1. 同步代码,立即执行
setTimeout(function() {
console.log('setTimeout'); // 5. 宏任务
}, 0);
Promise.resolve().then(function() {
console.log('promise1'); // 3. 微任务
}).then(function() {
console.log('promise2'); // 4. 微任务
});
console.log('script end'); // 2. 同步代码,立即执行
输出顺序:
- script start
- script end
- promise1
- promise2
- setTimeout
复杂场景执行顺序
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
setTimeout(() => {
console.log('6');
}, 0);
}).then(() => {
console.log('7');
});
console.log('8');
输出顺序:
- 1 (同步)
- 4 (Promise构造函数同步执行)
- 8 (同步)
- 5 (微任务)
- 7 (微任务)
- 2 (宏任务)
- 3 (微任务,在宏任务2之后执行)
- 6 (宏任务)
异步编程
函数回调方式、promise方式、 generator方式、ansy函数方式
async/await 如何捕获异常
async function fn(){
try{
let a = await Promise.reject('error')
}catch(error){
console.log(error)
}
}
并发与并行的区别?
- 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
- 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
setTimeout、setInterval、requestAnimationFrame 各有什么特点?
requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:
1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
垃圾回收与内存泄漏
juejin.cn/post/694119… 变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
垃圾回收的方式
1)标记清除 2)引用计数
浏览器的垃圾回收机制
浏览器垃圾回收机制通过 标记清除 和 分代回收 等策略,实现了高效的内存管理。
哪些情况会导致内存泄漏
以下四种情况会造成内存的泄漏:
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
js放到head里和body里区别
根据浏览器渲染机制,html渲染是从上到到下执行
放到head里,那么浏览器会异步开一个线程加载js,如果下载完js,里面执行,,问题来了,如dom还没有加载完,而js有操作dom的 方法,就会报错
解决方法:
- 在js方法 window.onload、
- 在srcpit 标签添加 async属性
- js 延迟加载方式
javaScript脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
一般有以下几种方式:
- defer 属性:
- 动态创建 DOM 方式:
- 使用 setTimeout 延迟方法: 件
- 使用ES6模块的
import()动态导入语法,按需加载模块。
1. defer 属性
defer 属性告诉浏览器在解析HTML文档时异步加载脚本,但会等到文档解析完成后再执行脚本。多个带有 defer 属性的脚本会按顺序执行。
<script src="script.js" defer></script>
2. async 属性
async 属性告诉浏览器异步加载脚本,并在加载完成后立即执行。多个带有 async 属性的脚本执行顺序不确定。
<script src="script.js" async></script>
3. 动态创建 <script> 元素
通过JavaScript动态创建 <script> 元素并插入到DOM中,可以实现脚本的延迟加载。
window.onload = function() {
var script = document.createElement('script');
script.src = 'script.js';
document.body.appendChild(script);
};
4. setTimeout 或 setInterval
使用 setTimeout 或 setInterval 延迟执行脚本加载。
setTimeout(function() {
var script = document.createElement('script');
script.src = 'script.js';
document.body.appendChild(script);
}, 3000); // 延迟3秒加载
5 模块化加载(ES6 Modules)
使用ES6模块的 import() 动态导入语法,按需加载模块。
button.addEventListener('click', () => {
import('./module.js')
.then(module => {
module.someFunction();
})
.catch(err => {
console.error('模块加载失败', err);
});
});
总结
defer:适合需要按顺序执行的脚本。async:适合独立且不依赖其他脚本的脚本。- 动态创建
<script>元素:灵活控制加载时机。 Intersection Observer API:适合基于视口的延迟加载。requestIdleCallback:适合在浏览器空闲时加载脚本。- ES6模块:适合现代模块化开发。
deferh和 async对比总结
defer(延迟执行,保证按顺序执行)
- 下载行为:与HTML解析并行下载
- 执行时机:HTML解析完成后,按脚本在文档中的顺序执行
- DOM访问:可以安全访问完整DOM
- 执行顺序:保证多个defer脚本按定义顺序执行
- 适用场景:有依赖关系的多个脚本
async(异步执行,下载完立马执行,不保证顺序)
- 下载行为:与HTML解析并行下载
- 执行时机:下载完成后立即执行,不保证顺序
- DOM访问:执行时DOM可能还未解析完成
- 执行顺序:不保证多个async脚本的执行顺序
- 适用场景:独立的第三方脚本(如分析、广告)
| 特性 | defer | async |
|---|---|---|
| 下载行为 | 与HTML解析并行下载 | 与HTML解析并行下载 |
| 执行时机 | HTML解析完成后,按脚本在文档中的顺序执行 | 下载完成后立即执行,不保证顺序 |
| 执行顺序 | 保证多个defer脚本按定义顺序执行 | 不保证多个async脚本的执行顺序 |
| DOM访问 | 可以安全访问完整DOM | 执行时DOM可能还未解析完成 |
| 适用场景 | 有依赖关系的多个脚本 | 独立的第三方脚本(如分析、广告) |
什么是FOUC?如何避免FOUC?
FOUC现象分析
FOUC(文档样式短暂失效(Flash of Unstyled Content)),主要指的是样式闪烁的问题,由于浏览器渲染机制(比如firefox),在 CSS 加载之前,先呈现了 HTML,就会导致展示出无样式内容,然后样式突然呈现的现象。会出现这个问题的原因主要是 css 加载时间过长,或者 css 被放在了文档底部。
FOUC优化建议:
1.减少使用@import导入样式表。
2.不在文档尾部引入样式。
3.尽量使用link标签在head中引入。(当然link标签是一个只能在head中使用的标签,因此使用link必然是在head中的)