Js

3 阅读26分钟

一、数据类型

1、基础数据类型栈内存

Number、String、Boolean、Undefined、Null、Symbol(es6)、BigInt(es2020)

注意点:

  • Symbol:用于创建唯一标识符,避免属性名冲突let s1 = Symbol('foo');
  • BigInt:表示超过 Number 安全范围的超大整数100n, BigInt(9999999999999999)

2、引用数据类型堆内存

都属于 Object

  • 对象:{}

  • 数组:[]

  • 函数:function(){}

  • 日期:new Date()

  • 正则:/abc/

二、栈内存、堆内存

1、区别

  • 栈(Stack) :放基本数据类型,存的是值本身

  • 堆(Heap) :放引用数据类型存的是地址,真正内容在

2、特点

  • 栈内存

    • 空间小、速度快

    • 自动分配、自动释放

    • 按顺序存放,先进后出

    • 基本类型的值:Number、String、Boolean、Undefined、Null、Symbol、BigInt

  • 堆内存

    • 空间大、速度稍慢

    • 无序存放

    • 对象、数组、函数等复杂数据

    • 栈里只存堆的地址,真正内容在堆中

      image.png

      • 栈里存的是地址编号(#001、#002)

      • 真正的数据放在堆里

      • 栈通过地址指向堆

三、浅拷贝、深拷贝

1、浅拷贝

  • 定义: 只拷贝第一层
  • 浅拷贝方式:
    • 展开运算符...
    • Object.assign
    • 数组slice oldArr.slice()
    • 数组concat oldArr.concat()

2、深拷贝

  • 定义:所有层级都拷贝(完全独立)
  • 深拷贝方式:
    • JSON.parse(JSON.stringify()) 不能拷贝函数、undefined、Symbol、循环引用;
    • 三方库Lodash const newObj = _.cloneDeep(oldObj)
    • 结构化克隆 structuredClone()(ES 新 API):
      • 使用:structuredClone(oldObj)
      • 优点:原生、支持大部分类型、无循环引用问题
      • 缺点:不支持函数
    • 手写:
      function deepCopy(obj) {
        if(obj === null || typeof obj !== 'object') return obj;
        let newObj = Array.isArray(obj) ? [] : {};
        for(let key in obj) {
          newObj[key] = deepCopy(obj[key]);
        }
        return newObj;
      }
      

四、数据类型判断

1、typeof

作用:判断 基本类型 + 函数*

typeof 123          // 'number'
typeof 'abc'        // 'string'
typeof true         // 'boolean'
typeof undefined    // 'undefined'
typeof function(){} // 'function'

缺点

  • typeof null'object'(坑!)
  • 无法区分 对象、数组,都返回 'object'

2、Array.isArray ()

Array.isArray([]) // true Array.isArray({}) // false

3、instanceof (判断引用类型)

作用:判断 对象是谁的实例

[] instanceof Array // true 
{} instanceof Object // true 
function(){} instanceof Function // true

缺点:不能判断基本类型

适合:判断引用类型(数组、对象、日期等)

4、Object.prototype.toString.call ()

作用判断所有类型,包括内置对象

Object.prototype.toString.call(123)        // "[object Number]"
Object.prototype.toString.call('abc')      // "[object String]"
Object.prototype.toString.call([])         // "[object Array]"
Object.prototype.toString.call({})         // "[object Object]"
Object.prototype.toString.call(null)       // "[object Null]"
Object.prototype.toString.call(undefined)  // "[object Undefined]"

适合:需要精准判断所有类型时

总结:

    1. 判断基本类型、函数:👉 typeof
    1. 判断数组 👉 Array.isArray()
    1. 判断纯对象 {}

      js

      typeof obj === 'object' && obj !== null && !Array.isArray(obj)
      
    1. 最精准判断所有类型 👉 Object.prototype.toString.call()

五、数组、伪数组/类数组

  • 伪数组/类数组 :

    结构长得像数组的普通对象,有数字下标 + 有 length 属性

  • 常见 : argumentsdocument.querySelectorAll 获取的 DOM 集合

    let fakeArray = {
      0: 'a',
      1: 'b',
      2: 'c',
      length: 3
    }
    

注意点:arguments类数组,数据格式是

function fn() {
  console.log(arguments) // arguments函数自带的,存放参数
}
fn(1, 2, 3)

image.png

六、var、let、const 的区别

1、var:函数级作用域,可重复声明,存在变量提升

    ```js 
    
    1、函数作用域,,不认 if /for/ 大括号 {}
    
      if (true) {
        var a = 10
      }
      console.log(a) // 10 ✅ 能访问到!
      
    2、只有函数能拦住
    
      function fn() {
         var a = 10
      }
      console.log(a) // 报错 ❌ 访问不到
      
    3、可重复声明
    
      var a = 1
      var a = 2
      var a = 3
      console.log(a) // 3 ✅ // 重复声明,报错
    ```

变量提升:

image.png

2、let:块级作用域,不可重复声明,存在暂时性死区

// 直接报错,暂时性死区
console.log(a); // 报错:Cannot access 'a' before initialization
let a = 10;

3、const:块级作用域,声明必须赋值,不可修改引用(但对象可改属性)

七、闭包

函数嵌套函数,内部函数引用了外部函数的变量,就形成了闭包

1、作用:

  • 让函数外部能访问函数内部的变量
  • 让变量一直保存在内存中,不会被销毁
// 外部函数
function outer() {
  // 外部函数的变量
  let num = 100

  // 内部函数(闭包核心)
  function inner() {
    // 内部函数 使用了 外部变量
    console.log(num)
  }

  // 把内部函数 return 出去
  return inner
}

// 接收返回的 inner 函数
const fn = outer()
fn() // 100 ✅ 能访问到 outer 里的 num
// 为什么能一直累加?因为闭包让 `count` **常驻内存**,不会消失
function add() {
  let count = 0 // 这个变量不会被销毁!

  return function() {
    count++
    console.log(count)
  }
}

const fn = add()

fn() // 1
fn() // 2
fn() // 3

函数add执行的流程

  1. const fn = add()

    • 执行 add()
    • 创建 count = 0
    • 返回内部函数
    • add 执行完毕,再也不执行了!
  2. fn()

    • 执行的是:return 出来的那个函数
    • 根本不会再进 add 里面
    • 不会重新创建 count = 0

总结:

闭包会导致内存泄漏

八、数组常用的方法 (增删改排序)

  • 1、push (末尾) arr.push(3)
  • 2、unshift (开头) arr.unshift(3)
  • 3、pop (删除最后一个) arr.pop()
  • 4、shift (删除第一个) arr.shift()
  • 5、splice (从哪删,删几个) arr.splice(1,1) // 从下标1开始,删1个 → [1]
  • 6、concat (合并数组) arr.concat([4,5])
  • 7、join(数组转字符串) arr.join('-') // "1-2-3"
  • 8、sort(排序) arr.sort((a,b) => a-b) // 正序
  • 9、reverse(反转) arr.reverse() [2,1,3].reverse() =>[3,1,2]
  • 10、slice(截取) arr.slice(1,3) // 开始,结束(不包含),截取一段,不影响原数组

注意点:不会改变原数组的是->concat、join、slice

九、遍历数组的方法

  1. forEach()
  2. map()
  3. filter()
  4. find()
  5. findIndex()
  6. some() 有一个满足条件 就返回 true
  7. every() 全部满足条件 才返回 true
  8. reduce()
reduce(回调函数,初始值);
// 回调函数
('上一次计算结果',item)=>{}
// 初始值
从什么值开始累加 ()
const sum = [0,1,2].reduce((total, item) => total + item, 3)
sum = 6
  1. for循环(breakcontinuereturn)
function testLoop() {
  const arr = [1, 2, 3, 4, 5];
  for (let i = 0; i < arr.length; i++) {
    if (i === 2) {
      // 在这里分别测试 break、continue、return
      // break; 跳出整个循环
      // continue; 跳出本次,进入下一次
      // return; 结束整个函数
    }
    console.log(arr[i]);
  }
  console.log("循环结束,继续执行函数后面的代码");
}
testLoop();

10.for…in((key==下标,适合遍历对象)

for (let key in arr) {
  console.log('值:', arr[key]);
}

11.for…of(breakcontinue)

方法性能能否提前退出性能差异根本原因
for 循环最快纯原生循环,无函数调用、无额外逻辑、无封装,CPU 执行成本最低
for…of很快遍历数组时会被引擎直接优化为接近原生 for 循环的执行逻辑,无明显额外开销(for of是语法,引擎能识别)
find / findIndex / some / every回调函数调用开销,但满足条件提前终止遍历,实际执行次数少
reduce中等不能有回调开销 + 累计值读写,必须完整遍历,无提前退出机会
forEach / map / filter较慢不能每一项都产生函数调用 + 作用域切换开销,且必须完整遍历全部元素
for…in最慢会遍历原型链、key 转为字符串、做大量属性检查,冗余操作极多

十、switch(多条件判断===if else)

let score = 85;
switch (true) {
  case score >= 90:
    console.log('优秀');
    break;
  case score >= 80:
    console.log('良好');
    break;
  case score >= 60:
    console.log('及格');
    break;
  default:
    console.log('不及格');
}

十一、js中假值

JS 规定:只有下面这 6 种值是假值(falsy) ,除此之外全是真值

  1. false
  2. 0-0
  3. ''""(空字符串)
  4. null
  5. undefined
  6. NaN

十二、运算符

1、=====

  • ==会转换数据类型

  • null /undefined 特殊情况

    console.log(null == undefined);   // true  → == 认为它们相等
    console.log(null === undefined);  // false → === 类型不同,不相等
    
  • 空字符串0 陷阱

    console.log('' == 0); // true 
    console.log('' === 0); // false
    

2、!=!==

差异:!=会先进行类型转换

3、!!! (布尔判断、取反)

4、?.????=||&&

  • ?.:

    // 找不到user返回undefined,往下也如此
    const name = user?.info?.name;
    
  • ??: 和||类似

    //  只有`null`、`undefined`时,`??`才取默认值
    false ?? true // false
    null ?? '默认' // '默认'
    undefined ?? '默认' // '默认'
    

5、++--

  • i++

    // **返回原来的值**,再自身 +1
        let i = 5
        let a = i++
    
        console.log(a) // 5(先赋值)
        console.log(i) // 6(后自增)
    
  • ++i

    // 先**自身 +1**,再返回新值
    let i = 5
    let a = ++i
    
    console.log(a) // 6(先自增,再赋值)
    console.log(i) // 6
    

十三、对象创建的方式

1. 字面量

const obj = {}

  • 本质:new Object() 的语法糖
  • obj.__proto__ === Object.prototype

2. new Object()

const obj = new Object()

  • 标准构造函数实例化

  • obj.__proto__ === Object.prototype

十四、原型、原型链

1、原型

简单:每个对象都有 __proto__,指向构造函数的 prototype

更准确的说:JS 里一切对象(实例函数数组普通对象)都有 proto,都指向 “创建它的那个构造函数” 的 prototype

2、原型链

每个对象都有 __proto__,指向构造函数的 prototype,而prototype也是个对象也有 __proto__。一直往上找,直到 __proto__ === null,这条链路就是原型链

十五、构造函数

1、定义构造函数

// 约定:构造函数名首字母大写(只是规范,不是强制)
function Person(name, age) {
  // this 指向即将创建的新对象
  this.name = name;
  this.age = age;
}
// 定义公有方法
Person.prototype.sayHi = function() {}

2、特点

  • 首字母大写:只是约定,方便一眼认出是构造函数

  • 内部用 this:给未来的对象添加属性 / 方法

  • 不用 return:默认返回新对象

  • 可以传参,实现批量创建同类对象

3、和普通函数区别

  • 调用时前面加个new

  • 构造函数优势:原型共享 + 统一类型识别

// 普通函数
function createPerson(name) {
  const obj = {};       // 手动创建
  obj.name = name;
  obj.say = function() {
    console.log(this.name);
  };
  return obj;            // 手动返回
}

const p1 = createPerson('张三');
const p2 = createPerson('李四');

缺点:每次调用都**新建一个 say 方法**,浪费内存

4、现在替代写法

// JS 现在更推荐用 class,本质还是构造函数,只是写法更优雅:
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHi() {
    console.log(this.name);
  }
}
const p = new Person('王五', 22);

为什么class更好,简要分析下,后面会细讲

  • 写法清晰,语义更强

    一眼看出非普通函数、constructor负责初始化、结构更清晰

  • 继承简单,用 extends

    // Es5原型继承 = this指向+子赋值父prototype+子constructor重新赋值子构造器

    function Person(name, age) {
      this.name = name
      this.age = age
    }
    Person.prototype.sayHi = function() {
      console.log(`Hi, I'm ${this.name}`)
    }
    // 1. 写子类构造函数
    function Student(name, age, score) {
      // 2. 借用父类构造函数(手动绑定 this)
      Person.call(this, name, age)
      // 3. 加自己的属性
      this.score = score
    }
    // 4. 手动绑定原型链(最容易写错的地方)
    Student.prototype = Object.create(Person.prototype)
    Student.prototype.constructor = Student
    // 5. 给子类加方法
    Student.prototype.study = function() {
      console.log(`${this.name} 考了 ${this.score} 分`)
    }
    // 6.调用
    const s = new Student('小明', 18, 100)
    s.sayHi() // 继承来的
    s.study()
    

    // Class = extends+super

    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    
      sayHi() {
        console.log(`Hi, I'm ${this.name}`);
      }
    }
    
    // 子类继承(一句话)
    class Student extends Person {
      constructor(name, age, score) {
        super(name, age); // 自动调用父类构造
        this.score = score;
      }
    
      study() {
        console.log(`${this.name} 考了 ${this.score} 分`);
      }
    }
    

十六、new关键字

1、创建一个空的全新对象 const obj = {};

2、链接原型

把这个空对象的 __proto__ 指向构造函数的 prototype

obj.__proto__ = Person.prototype;

3、绑定 this

执行构造函数,并把空对象绑定为构造函数里的 this

4、返回实例对象

  • 如果构造函数没有手动返回对象 / 函数,就自动返回第 1 步创建的对象

  • 如果构造函数手动返回了对象 / 函数,则以手动返回的为准(基本类型无效,仍返回新对象)。

总结:

1.造空对象 → 2. 连原型 → 3. 绑 this → 4. 返对象

function myNew(constructor, ...args) {
  // 1. 创建空对象
  const obj = {};
  // 2. 链接原型
  obj.__proto__ = constructor.prototype;
  // 3. 绑定 this 并执行构造函数
  const res = constructor.apply(obj, args);
  // 4. 返回:如果构造函数返回对象则用它,否则返回新对象
  return res instanceof Object ? res : obj;
}

// 测试
const p2 = myNew(Person, "李四", 20);
p2.sayHi(); // 你好,我是李四

十七、this关键字

1、全局环境 / 普通函数直接调用 → 指向 window(浏览器)

// 全局 this
console.log(this); // window

// 普通函数
function fn() {
  console.log(this);
}
fn(); // window

意思:没人指定调用者,默认就是全局对象 window

2、对象方法调用 → 指向调用该方法的对象

const obj = {
  name: "小明",
  say: function () {
    console.log(this.name);
  }
};

obj.say(); // 小明 → this 指向 obj

3、构造函数 + new → 指向 新创建的实例对象

function Person(name) {
  this.name = name; // this 是新实例
}

const p = new Person("小红");
console.log(p.name); // 小红

4、call /apply/bind → 指向 你传入的第一个参数

function fn() {
  console.log(this);
}

const obj = { a: 1 };

fn.call(obj);   // this → obj
fn.apply(obj);  // this → obj
fn.bind(obj)(); // this → obj

5、箭头函数 → 指向 外层作用域的 this

const person = {
  name: '张三',
  say: function () {
    // 普通函数,this = person

    setTimeout(() => {
      // 箭头函数,this指向外层say的this
      console.log(this.name); // 张三
    }, 1000);
  }
};
person.say()

// 箭头改成普通函数
const person = {
  name: '张三',
  say: function() {
    setTimeout(function() {
      console.log(this.name) 
// undefined,因为 this 变成 window 了(无调用者)
    }, 1000)
  }
}

总结:

  • 普通函数:谁调用我,我指向谁

  • 构造函数:new 我,我指向新对象

  • call/apply/bind:你让我指向谁,我就指向谁

  • 箭头函数:我没有 this,我继承外面的 this

  • 全局 / 自己调用:指向 window

十八、call、apply、bind

1、call 立刻执行函数,手动改 this,参数一个个传

fn.call(this指向的对象, 参数1, 参数2, ...)

2、apply 立刻执行函数,手动改 this,参数用数组传

fn.apply(this指向的对象, [参数1, 参数2, ...])

3、bind 不立刻执行,返回一个永久绑定 this的新函数

const newFn = fn.bind(this指向的对象, 参数1, 参数2, ...)

总结:

const obj = { name: 'obj' }

function fn(a, b) {
  console.log(this, a, b)
}
  • fn.call(obj, 1, 2) → 立即执行,this=obj

  • fn.apply(obj, [1,2]) → 立即执行,this=obj

  • const newFn = fn.bind(obj,1,2) → 不执行,返回新函数

  • newFn() → 执行,this 永远是 obj

十九、什么是作用域?

  • 全局作用域

  • 函数作用域

  • 块级作用域(let/const)- ES6 新增

    {} 包裹的代码块(if、for、while、switch 等)

    if (true) {
      // 块级作用域
      let a = 10;
      const b = 20;
      var c = 30; // var 没有块级作用域
    }
    
    console.log(a); // 报错 ❌
    console.log(b); // 报错 ❌
    console.log(c); // 30 ✔️
    
  • 模块作用域-ES6 - 新增

    使用 export / import 的独立 JS 模块

    // a.js 模块
    let msg = "模块私有";
    export function say() { console.log(msg); }
    
    // b.js
    import { say } from './a.js';
    say(); // 可以用
    console.log(msg); // 报错 ❌
    

作用域链:

  • 先找当前作用域

  • 找不到 → 找外层作用域

  • 一直找到全局作用域

  • 都找不到 → 报错 is not defined

    let global = "全局";
    function outer() {
      let outerVal = "外层";
    
      function inner() {
        let innerVal = "内层";
        // 查找顺序:inner → outer → global
        console.log(innerVal); // 内层
        console.log(outerVal); // 外层
        console.log(global);   // 全局
      }
      inner();
    }
    outer();
    

二十、什么是变量提升?

var 声明的变量会被提升到作用域顶部,但赋值不提升

let/const 不存在变量提升,存在暂时性死区

二十一、箭头函数和普通函数区别?

  • 没有自己的 this

  • 没有 arguments

  • 不能当构造函数

  • 没有 prototype

二十二、什么是原型继承?

// 通过原型链让子类拥有父类的属性和方法
Child.prototype = new Parent()

二十三、Promise 有哪些状态?

  • pending

  • fulfilled

  • rejected

二十四、Promise 常用方法?

  • .then()

  • .catch()

  • .finally()

  • Promise.all()

    • 全部 fulfilled → 返回结果数组

    • 任意一个 rejected → 立即失败,进入 catch

  • Promise.allSettled()

    • 所有 Promise 结束后才返回

    • 返回数组,每个对象形如:

    • 成功:{ status: 'fulfilled', value: ... }

    • 失败:{ status: 'rejected', reason: ... }

二十五、Es6新增属性、方法

1、变量声明

let / const

2、解构赋值

  • 对象解构 const { name, age } = user;
  • 数组解构 const [a, b, c] = arr;
  • 函数参数解构 function fn({ name }) { console.log(name); }

3、字符串新增方法

  • includes(str) :判断是否包含指定字符串,返回布尔值

  • startsWith(str) :判断是否以指定字符串开头

  • endsWith(str) :判断是否以指定字符串结尾

  • repeat(n) :重复字符串 n 次

  • 模板字符串 ` `${变量}` :支持换行、直接拼接变量

4、数组新增方法

  • 扩展运算符 ...

    const arr = [1,2,3];
    const newArr = [...arr]; // 拷贝
    const arr2 = [...arr, 4,5]; // 合并
    
  • Array.from(类数组) :把类数组 / 伪数组转成真正的数组

    // 可迭代对象-  能被for of遍历的
    Array.from('hello') // ['h','e','l','l','o']
    
  • Array.of() :创建数组(避免 new Array () 歧义)

    Array.of(1,2,3) // [1,2,3]
    
  • find(callback) :返回第一个符合条件的元素

  • findIndex(callback) :返回第一个符合条件的索引

  • some(callback) :只要一个满足就返回 true

  • every(callback) :全部满足才返回 true

  • flat(n) :数组扁平化(n 为深度,默认 1,Infinity 全拍平)

    const arr = [1, [2, [3, 4]]];
    
    arr.flat();        // 默认只拍平 1 层
    // [1, 2, [3,4]]
    
    arr.flat(2);       // 拍平 2 层
    // [1,2,3,4]
    

5、函数新增

  • 箭头函数 () => {}
  • 函数默认参数 function fn(a = 10) {}
  • rest 参数 ...args 接收剩余参数: function fn(...args) {}

6、对象新增

  • 简洁表示法

    const name = "小明";
    const obj = { name, fn(){} }; 
    // 等同于 {name:name, fn:function(){}}
    
  • 属性名表达式

    const key = "age";
    const obj = { [key]: 20 };
    
  • 对象方法

    • Object.assign(目标, 源1, 源2) :合并 / 拷贝对象

    • Object.keys(obj) :获取所有键

    • Object.values(obj) :获取所有值

    • Object.entries(obj) :转成 [[key,val],...]

      const entries = Object.entries({ name: '张三', age: 20, gender: '男' });
      console.log(entries);
      
      打印:
      [
        ['name', '张三'],
        ['age', 20],
        ['gender', '男']
      ]
      

7、Set / Map 数据结构

  • Set 去重

    const arr = [1,2,2,3];
    const s = new Set(arr);
    const newArr = [...s]; // [1,2,3]
    

    方法:add()delete()has()clear()

    s.add(2);
    s.delete(2); // 删除集合里的某个元素
    console.log(s.has(1)); // true **判断是否包含**某个值
    s.clear(); // 清空整个 Set
    
  • Map 键可以是任意类型

    Map 的 key 可以是对象、函数、DOM 元素

    const m = new Map();
    m.set("name", "小明");
    m.get("name");
    

8、Promise

```js
// 解决回调地狱,处理异步请求
new Promise((resolve,reject)=>{
  setTimeout(()=>resolve(1))
}).then(res=>{})
```

9、class 类(面向对象)

class Person {
  constructor(name){ this.name = name; }
  say(){}
}

10、模块化 import /export

export default fn;
import fn from './a.js';

高频速记清单(面试 / 工作核心)

  1. let / const
  2. 解构赋值、模板字符串
  3. 扩展运算符 ...
  4. 数组:findflatincludes
  5. 对象:Object.assignkeys/values/entries
  6. 箭头函数
  7. Set 数组去重
  8. Promise
  9. class
  10. import/export

二十六、继承(实现继承的几种方式)

1、原型链继承:

function Parent() {
  this.name = 'Parent';
}

function Child() {
  this.age = 10;
}

Child.prototype = new Parent();

const child = new Child();
console.log(child.name); // 可以访问到父类的属性

2、构造函数继承:

function Parent(name) {
  this.name = name;
}

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

const child = new Child('Child', 10);
console.log(child.name); // 可以访问到父类的属性

3、ES6 Class继承:

class Parent {
  constructor(name) {
    this.name = name;
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
}

const child = new Child('Child', 10);
console.log(child.name); // 可以访问到父类的属性

4、组合继承(原型链继承和构造函数继承的结合):

function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello, ' + this.name);
}

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

const child = new Child('Child', 10);
console.log(child.name); // 可以访问到父类的属性
child.sayHello(); // 可以调用父类的方法

image.png

二十七、script标签中的async和defer的作用

这两个属性都是用来控制外部脚本(必须带 src)的加载和执行时机,解决JS 阻塞 HTML 解析和渲染的问题,让页面加载更流畅。

核心前提:

浏览器解析 HTML 时,默认遇到 <script src="..."> 会停止解析 HTML,先下载脚本 → 执行脚本 → 再继续解析 HTML,容易导致页面白屏。

1、默认情况(无 async/defer)

<script src="script.js"></script>
  1. 浏览器解析 HTML 到这里
  2. 暂停解析 HTML
  3. 立即下载 JS 文件
  4. 下载完成后立即执行 JS
  5. 执行完毕,才继续解析 HTML

❌ 缺点:阻塞页面渲染,大脚本会让页面长时间白屏。

2、defer 属性(延迟执行)

<script src="script.js" defer></script>
核心特点
  1. 异步下载:下载 JS 时,浏览器不停止解析 HTML
  2. 延迟执行等整个 HTML 文档完全解析完成后,再执行 JS
  3. 按顺序执行:多个带 defer 的脚本,按照书写顺序依次执行
  4. 只对外部脚本生效(必须有 src)
执行顺序图解

HTML解析 → 并行下载JS → HTML解析完毕 → 执行JS

3、async 属性(异步执行)

<script src="script.js" async></script>

核心特点

  1. 异步下载:下载 JS 时,浏览器不停止解析 HTML
  2. 立即执行下载完成后立刻执行,不管 HTML 是否解析完
  3. 乱序执行:多个带 async 的脚本,谁先下载完谁先执行
  4. 只对外部脚本生效
  5. 如果同时写 async defer浏览器优先使用 async

执行顺序图解

HTML解析 → 并行下载JS → JS下载完成 → 暂停HTML,执行JS → 继续解析HTML

使用建议:

  • 用 defer:脚本需要操作 DOM / 依赖其他脚本 / 必须按顺序执行(如框架、插件)

  • 用 async:脚本完全独立,不依赖任何代码,也不操作页面 DOM(如统计、埋点)

  • 都不用:小体积脚本,或脚本必须在某个元素前执行

总结:

  • defer:异步下载,HTML 解析完再执行,保顺序

  • async:异步下载,下载完立刻执行,乱序

  • 核心作用:避免 JS 阻塞 HTML 解析,提升页面加载速度

二十八、 http和https的区别

http:浏览器和服务器之间约定好的 “说话规则”

  • HTTP:快、不安全、明文

  • HTTPS:加密、安全、需要证书、现在互联网主流

注意点:TCP/IP 负责通,HTTP 负责内容格式。

二十九、 Tcp/Ip三次握手,四次挥手

三次握手(建立连接)

  1. 客户端发:我要连
  2. 服务端回:可以,你确认下
  3. 客户端再回:收到,连接成功

四次挥手(断开连接)

  1. 客户端发:我发完了,要关
  2. 服务端回:知道了,我收尾
  3. 服务端发:我也发完了,可以关
  4. 客户端回:收到,断开连接

三十、 hash和history模式的区别

  • url显示:hash携带“#”,history不携带
  • 路由更新原理:hash是通过监听hashChange,history是监听popState事件
  • 兼容性:hash支持在不同浏览器和服务器环境下,history不支持低版本的浏览器(能兼容到IE10) 注意点:
// hash window.location.hash+hashchange
window.addEventListener('hashchange', () => {
  // 拿到最新的 hash
  const path = window.location.hash.slice(1)
  // 匹配路由 → 渲染组件
})
history是靠pushState +replaceState
history.pushState({}, '', '/home')
history.replaceState({}, '', '/login')
// pushState和replaceState类似,但replaceState不会新增记录,不能后退
  • 服务器端配置支持:history需要服务器配置(在刷新页面时,它会向服务器发送 GET 请求,但此时服务器并没有配置相应的资源来匹配这个请求,因此返回 404 错误)---blog.csdn.net/qq_38290251…

image.png

image.png

三十一、同源策略

image.png

同源策略是浏览器的核心,如果没有同源策略会遭受网络攻击。

主要指的是 协议+域名+端口号 三者一致

备注:非同源会引起跨域。如何处理:

  • CORS(跨源资源共享)

    • 服务器在响应头添加 Access-Control-Allow-Origin,明确允许哪些域访问。
    • 最标准、最推荐
  • JSONP

    • 利用 <script> 可跨域嵌入的特性,仅支持 GET 请求。

      <script>
        // 提前定义好一个函数
        function getData(res) {
          console.log('拿到跨域数据:', res)
        }
      </script>
      <script src="http://localhost:3000/api/jsonp?callback=getData"></script>
      
  • 代理服务器(Proxy)

    • 前端 → 同源代理 → 转发到目标跨域服务器。
    • 开发常用:Webpack Dev Server、Vite Proxy。

三十二、浏览器缓存策略

强缓存(本地缓存)/ 协商缓存(弱缓存)。

强缓:不发起请求,直接使用缓存里的内容,浏览器把js css image等存到内存中,下次用户访问直接从内存中取,提高性能。

协缓:需要向后台发送请求,通过判断来决定是否使用缓存,如果请求内容没有变化,则返回304,浏览器就用缓存里的内容。

三十三、事件机制

JavaScript 事件机制是浏览器与用户交互的核心原理,描述了事件在 DOM 元素之间的传递规则,是前端开发必须掌握的核心知识点。

事件流:事件(点击、输入、滚动、鼠标移动等)发生后,不会只作用于目标元素,而是会在DOM 树的元素之间传递,这个传递过程就是事件流

1、三个阶段(W3C 标准)

事件触发后,会完整经历 3 个阶段(捕获阶段 → 目标阶段 → 冒泡阶段):

  1. 捕获阶段(Capture Phase)事件从最外层根元素(window/document) 向内传递,直到触发事件的目标元素
  2. **目标阶段(Target Phase)**事件到达用户实际操作的元素(点击 / 输入的元素),执行该元素的事件监听。
  3. 冒泡阶段(Bubbling Phase)事件从目标元素向外传递,回到最外层根元素。

2、两个核心传播模式

  • 事件冒泡(默认模式)

    • 特点:事件由内向外传递(子元素 → 父元素 → 祖先元素 → document → window)
    • 默认行为:我们平时写的 addEventListener 不写第三个参数,默认就是冒泡模式。

    代码示例

    <div id="parent">
      父元素
      <button id="child">子按钮</button>
    </div>
    
    <script>
      const parent = document.getElementById('parent');
      const child = document.getElementById('child');
    
      // 冒泡监听(第三个参数 false/不写)
      parent.addEventListener('click', () => {
        console.log('父元素 冒泡点击');
      });
      child.addEventListener('click', () => {
        console.log('子元素 冒泡点击');
      });
      // 点击按钮:先打印 子元素 → 再打印 父元素
    </script>
    
  • 事件捕获

    • 特点:事件由外向内传递(window → document → 祖先元素 → 父元素 → 子元素)
    • 用法addEventListener 第三个参数传 true

    代码示例

    js

    // 捕获监听
    parent.addEventListener('click', () => {
      console.log('父元素 捕获点击');
    }, true); // 开启捕获
    
    child.addEventListener('click', () => {
      console.log('子元素 捕获点击');
    }, true);
    // 点击按钮:先打印 父元素 → 再打印 子元素
    

3、关键 API

  • 阻止事件冒泡

    event.stopPropagation():阻止事件继续向外 / 向内传播,只执行当前元素的事件。

    js

    child.addEventListener('click', (e) => {
      e.stopPropagation(); // 阻止冒泡
      console.log('子元素点击');
    });
    // 点击按钮:只打印子元素,父元素不会触发
    
  • 阻止默认行为

    event.preventDefault():阻止浏览器自带的默认事件(如表单提交、a 标签跳转、右键菜单)

    js

    const link = document.querySelector('a');
    link.addEventListener('click', (e) => {
      e.preventDefault(); // 阻止a标签跳转
      console.log('阻止跳转');
    });
    
  • 事件委托(利用冒泡)

    核心用途:给父元素绑定事件,利用冒泡机制监听子元素触发的事件。优点

    • 节省内存(不用给每个子元素绑事件)
    • 支持动态生成的元素(新增子元素自动生效)

    示例

    <ul id="list">
      <li>选项1</li>
      <li>选项2</li>
    </ul>
    
    <script>
      // 给父ul绑定事件,委托给所有子li
      document.getElementById('list').addEventListener('click', (e) => {
        // 判断点击的是不是li
        if(e.target.tagName === 'LI'){
          console.log('点击了:', e.target.textContent);
        }
      });
    </script>
    

总结:同时绑定捕获 + 冒泡时,执行规则:

window捕获 → document捕获 → 父元素捕获 → 子元素(目标)捕获/冒泡 → 父元素冒泡 → document冒泡 → window冒泡

三十四、排队机制

所有任务都要排队执行,先同步、后异步,异步任务分优先级排队

1、3 个关键队列

JS 执行任务时,会维护 3 个「排队队列」,严格按顺序执行:

  1. 调用栈(同步队列) :执行同步代码(直接运行的代码,无延迟)
  2. 微任务队列(高优先级异步) :执行微任务(优先级最高,插队执行)
  3. 宏任务队列(低优先级异步) :执行宏任务(普通异步,最后执行)

2、执行规则

  • 先清空 调用栈 所有同步任务

  • 再清空 微任务队列 所有微任务

  • 最后取 宏任务队列第一个宏任务执行

  • 循环往复(执行完一个宏任务,立刻再检查微任务)

3、宏任务 vs 微任务

  • 宏任务(普通异步,排队靠后)

    • setTimeout / setInterval(定时器)
    • 接口请求(fetch/axios/ajax
    • DOM 事件(点击、滚动)
    • I/O 操作(文件读取)
  • 微任务(高优先级异步,插队执行)

    • Promise.then() / Promise.catch() / Promise.finally()
    • async/await(底层就是 Promise 微任务)
    • queueMicrotask()

注意点:

  • 微任务:完全由 JS 引擎自己 产生并管理
  • 宏任务:由 浏览器宿主环境 产生(浏览器给 JS 塞进来的)

4、常见面试题

  • 第 1 题(基础版,和你给的一模一样)

    console.log('1');
    
    setTimeout(() => {
      console.log('2');
    }, 0);
    
    Promise.resolve().then(() => {
      console.log('3');
    });
    
    console.log('4');
    

    输出顺序

    1 → 4 → 3 → 2

    解析

    1. 先执行所有同步:1、4
    2. 再清空微任务:3
    3. 最后执行宏任务:2

  • 第 2 题(多个微任务 + 多个宏任务)

    console.log('1');
    
    setTimeout(() => console.log('2'), 0);
    setTimeout(() => console.log('3'), 0);
    
    Promise.resolve().then(() => console.log('4'));
    Promise.resolve().then(() => console.log('5'));
    
    console.log('6');
    

    输出顺序

    1 → 6 → 4 → 5 → 2 → 3

    解析

    • 同步先跑:1、6
    • 微任务全部清空:4、5
    • 宏任务按顺序执行:2、3

  • 第 3 题(async /await 必考)

    async function fn() {
      console.log('1');
      await Promise.resolve();
      console.log('2');
    }
    
    console.log('3');
    fn();
    console.log('4');
    

    输出顺序

    3 → 1 → 4 → 2

    解析

    • await 前面是同步:1
    • await 后面是微任务:2
    • 同步顺序:3 → 1 → 4
    • 最后微任务:2

  • 第 4 题(宏任务里产生微任务)

    console.log('1');
    
    setTimeout(() => {
      console.log('2');
      Promise.resolve().then(() => console.log('3'));
    }, 0);
    
    Promise.resolve().then(() => console.log('4'));
    
    console.log('5');
    

    输出顺序

    1 → 5 → 4 → 2 → 3

    解析

    • 同步:1、5
    • 微任务:4
    • 宏任务:2
    • 宏任务执行完 → 立刻检查微任务 → 3

  • 第 5 题(综合最难面试题)

    console.log('1');
    
    setTimeout(() => {
      console.log('2');
      Promise.resolve().then(() => console.log('3'));
    }, 0);
    
    Promise.resolve().then(() => {
      console.log('4');
      setTimeout(() => console.log('5'), 0);
    });
    
    console.log('6');
    

    输出顺序

    1 → 6 → 4 → 2 → 3 → 5

三十五、Promise

Promise 是 JS 用来处理异步操作的对象,让回调地狱(多层嵌套)变成链式调用,代码更干净、更好读。

1、回调地狱

// 第一层:请求用户
getUser(function(user) {
  // 第二层:用 userId 查订单
  getOrder(user.id, function(order) {
    // 第三层:查商品
    getGoods(order.goodsId, function(goods) {
      console.log(goods)
    })
  })
})
// 每一步都要等上一步完成,所以每一步都要写在上一步的回调里,这就形成了**回调地狱**

2、Promise 三种状态

Promise 一生只有 3 种状态,且一旦改变就不能再变

  1. pending(等待中)—— 刚开始,还没结果
  2. fulfilled(成功)—— 操作完成,调用 resolve ()
  3. rejected(失败)—— 操作出错,调用 reject ()
// 创建 Promise
const p = new Promise((resolve, reject) => {
  // 这里做异步操作
  setTimeout(() => {
    const success = true

    if (success) {
      resolve('成功啦!') // 成功 → 把结果传给 then
    } else {
      reject('失败了~')   // 失败 → 把错误传给 catch
    }
  }, 1000)
})
// 使用 Promise
p.then(res => {
  console.log(res) // 成功结果
}).catch(err => {
  console.log(err) // 失败错误
}).finally(() => {
  console.log('无论成功失败都会执行')
})

3、链式调用(最强大的地方)

// Promise 最牛的就是**链式写法**,彻底告别回调地狱
// 所有异步任务按顺序执行,代码扁平、清晰
request1()
  .then(res1 => request2(res1))
  .then(res2 => request3(res2))
  .then(res3 => console.log('全部完成'))
  .catch(err => console.log('任何一步出错都会进 catch'))

4、常用静态方法(工作高频)

  • Promise.all()

    等待所有 Promise 成功,一个失败就全部失败

    Promise.all([p1, p2, p3]).then(res => {
      // res 是 [p1结果, p2结果, p3结果]
    })
    
  • Promise.allSettled()

    不管成功失败,全部执行完再返回

5、async/await(Promise 的语法糖)

async function getData() {
  try {
    const res = await axios.get('/api')
    console.log(res)
  } catch (err) {
    console.log(err)
  }
}

三十六、try catch

1、能捕捉的错误

  • 变量未定义:a is not defined

  • 类型错误:null.xxxundefined.xxx

  • 调用不是函数的东西:123()

  • 访问不存在的下标等

  • 手动 throw 抛出的错误 throw new Error('出错了')

  • eval 里的同步错误

    try {
      eval('abc')
    } 
    
  • 绝大多数同步逻辑异常

只要是这一行代码立刻、马上、同步抛出的错误,try 包着就能 catch 到。

2、不能捕捉的错误

  • setTimeout 回调

  • setInterval 回调

  • 原生 AJAX onload/onerror

  • 事件监听 click

  • 语法错误

    语法错误:代码还没开始跑,浏览器一读就发现写得不对,直接拒绝执行

    // 代码还没开始跑,解析就挂了,根本进不去 try
    try {
      const a = // 语法错误
    } catch (e) {}
    
  • 全局 / 跨域脚本错误(被浏览器限制)

    • 别的域名加载的 JS 报错
    • 报错信息会被模糊化,catch 拿不到详细堆栈
  • Promise 内部 reject(不使用 await 时)

    // Promise 错误属于**微任务异步错误**,不是同步抛出
    try {
      Promise.reject('err')
    } catch (e) {
      // 抓不到
    }
    
  • 堆栈溢出 :有时能抓到,有时直接崩,看浏览器实现,不依赖它捕获

三十七、堆栈溢出

堆栈溢出只在一种核心场景出现:函数调用层级太深,或无限递归,导致调用栈被塞满,放不下新的函数调用

1、递归没有中止条件

// 自己调用自己,无限套娃,永远停不下来
function fn() {
  fn()
}
fn()

2、递归有终止条件,但数据太大也会溢出

function factorial(n) {
  if (n === 1) return 1
  return n * factorial(n - 1)
}

factorial(100000) // 爆栈

3、两个函数互相调用,形成死循环

function a() { b() }
function b() { a() }
a()

4、极其深的原型链查找

5、极深的数组 / 对象解构、表达式嵌套

6、等等