Skip to content
On this page

八股文

01.JavaScript

javascript 的数据类型有哪些?

  • 基本数据类型
    • number
    • string
    • boolean
    • null
    • undefined
    • symbol
  • 引用数据类型
    • object

javascript 的变量有什么特点?

  • 变量是松散类型的,可以用来保存任何类型的数据
  • 变量没有类型限制,可以用来保存任意类型的数据
  • 变量可以重复声明

javascript 的变量命名有什么规范?

  • 变量名可以包含字母,数字,下划线,但是不能以数字开头
  • 变量名不能包含空格
  • 变量名不能包含 JavaScript 的关键字
  • 变量名不能包含 JavaScript 保留字

javascript 的变量提升有什么特点?

  • 变量提升是 JavaScript 的默认行为
  • 变量提升是 JavaScript 的静态分析
  • 变量提升是 JavaScript 的编译阶段

如何判断 javascript 的数据类型?

  • typeof
  • instanceof
  • constructor
  • Object.prototype.toString

javascript 的 typeof 有什么特点?

  • typeof 返回值是字符串
  • typeof 返回值是大小写敏感的
  • typeof 返回值是 typeof 操作符的返回值
  • typeof 返回值是 undefined
  • typeof 返回值是 boolean
  • typeof 返回值是 number
  • typeof 返回值是 string
  • typeof 返回值是 symbol

javascript 的 instanceof 有什么特点?

  • instanceof 返回值是布尔值
  • instanceof 返回值是 instanceof 操作符的返回值
  • instanceof 返回值是 false
  • instanceof 返回值是 true

javascript 的 constructor 有什么特点?

  • constructor 返回值是函数

怎么判断两个对象相等?如何判断空对象?

  • 判断两个对象相等
    • Object.is(obj1, obj2)
  • 判断空对象
    • Reflect.owneKeys(obj).length === 0

javascript 的 Object.prototype.toString 有什么特点?

  • Object.prototype.toString 返回值是字符串
  • Object.prototype.toString 返回值是 toString 操作符的返回值
  • Object.prototype.toString 返回值是 Object.prototype.toString.call()的返回值

JavaScript 中 (a ==1 && a== 2 && a==3) 可能为 true 吗?

  • 自定义 toString(或者 valueOf)方法,每次调用改变一次返回值,从而满足判断条件
js
const a = {
  i: 1,
  toString: function () {
    return a.i++
  },
}

tostring() 方法属于 Object 对象,当使用==时,如果两个类型不一样,那么 js 会自动转换类型,转换成相同类型,再进行比较。 在这里左边对象,右边数字的情况下,会首先尝试调用 valueOf()方法(如果可以调用的话)来将对象转为数字,如果失败,在调用 tostring()方法,如果还是失败,那么就报错。

undefined 和 undeclared 区别?

  • undefined 表示变量声明了,但没有赋值
  • undeclared 表示变量没有声明
  • 对于 undeclared 变量的引用,浏览器会报引用错误,如 ReferenceError: b is not defined 。但是我们可以使用 typ eof 的安全防范机制来避免报错,因为对于 undeclared(或者 not defined )变量,typeof 会返回 "undefined"。

JavaScript 中获取原型的方法

  • Object.getPrototypeOf()
  • proto

JavaScript 中如何判断一个变量是数组类型

  • Array.isArray()
  • instanceof
  • constructor
  • Object.prototype.toString.call()

在 JavaScript 中,使用 const 定义函数和直接用 function 声明函数有什么区别

在 JavaScript 中,使用const定义函数(函数表达式)和直接用function声明函数(函数声明)存在以下核心区别:


1. 提升(Hoisting)行为

  • 函数声明:通过function声明的函数会被提升到当前作用域的顶部,因此在声明前调用不会报错。例如:
    javascript
    foo() // 正常执行
    function foo() {}
    
  • const定义的函数表达式:不会被提升,若在声明前调用会触发ReferenceError
    javascript
    bar() // 报错:Cannot access 'bar' before initialization
    const bar = function () {}
    
    这一特性使得const定义的函数更符合“先定义后使用”的编程规范。

2. 作用域与重复声明

  • 函数声明:在非严格模式下,函数声明可能被重复声明(覆盖),但在严格模式或块级作用域(如if语句、循环)中会报错。例如:
    javascript
    function test() {}
    function test() {} // 非严格模式下覆盖,严格模式下报错
    
  • const定义的函数表达式:由于const的常量特性,同一作用域内不可重复声明,且遵循块级作用域规则。例如:
    javascript
    if (true) {
      const func = () => {}
    }
    func() // 报错:func未定义
    
    这一特性使const定义的函数更安全,避免意外覆盖。

3. 重新赋值能力

  • 函数声明:无法直接通过赋值修改,但可以通过重新声明覆盖(非严格模式下):
    javascript
    function fn() {}
    fn = 123 // 非严格模式下静默失败,严格模式下报错
    
  • const定义的函数表达式:完全禁止重新赋值,任何尝试都会触发TypeError
    javascript
    const fn = () => {}
    fn = 123 // 报错:Assignment to constant variable
    
    这一限制增强了代码的稳定性。

4. 函数名称与调试

  • 函数声明:函数名是显式声明的,调试时堆栈会显示函数名,便于追踪。
  • const定义的匿名函数表达式:若未命名,调试时可能显示为anonymous,但可通过命名函数表达式解决:
    javascript
    const fn = function namedFunc() {} // 堆栈显示namedFunc
    
    这一差异在复杂项目中可能影响调试效率。

5. 箭头函数与this绑定

  • 若使用const定义箭头函数,其this值由外层作用域决定,而function声明的函数具有独立的this绑定。例如:
    javascript
    const obj = {
      method: function () {
        console.log(this)
      }, // this指向obj
      arrowMethod: () => {
        console.log(this)
      }, // this指向外层(如window)
    }
    
    这一区别可能影响函数的使用场景。

总结

  • 优先使用const定义函数:在需要块级作用域、避免提升、防止意外覆盖时更安全。
  • 选择function声明:若需利用提升特性或明确函数名调试信息时适用。

两种方式的选择需结合具体场景,兼顾代码规范与功能需求。

JS 中本地对象、内置对象、宿主对象分别是什么,有什么区别?

在 JavaScript 中,对象根据其来源和定义方式可分为 本地对象内置对象宿主对象,它们的核心区别如下:


一、本地对象(Native Object)

定义
由 ECMAScript 规范定义、独立于宿主环境(如浏览器或 Node.js)实现的对象。这些对象是 JavaScript 语言本身的基础组成部分。
常见实例

  • ArrayDateRegExpFunctionObject 等构造函数创建的对象。
  • 例如:new Array() 生成的数组实例。
    特点
  • 由 JavaScript 引擎直接提供,与运行环境无关。
  • 开发者可通过 new 关键字显式创建实例。

二、内置对象(Built-in Object)

定义
属于本地对象的一个子集,特指 ECMAScript 规范中定义的、无需实例化即可直接使用的对象。
仅包含两类

  1. Global 对象
    • 在浏览器中体现为 window(全局作用域),在 Node.js 中为 global
    • 包含全局函数(如 parseInteval)和全局属性(如 InfinityNaN)。
  2. Math 对象
    • 提供数学运算的静态方法和常量(如 Math.random()Math.PI)。
      特点
  • 无需通过 new 创建,直接通过对象名调用。
  • 是语言层面的“预定义工具”。

三、宿主对象(Host Object)

定义
由 JavaScript 的宿主环境(如浏览器或 Node.js)提供的对象,与 ECMAScript 规范无关。
常见实例

  • 浏览器环境
    • BOM(浏览器对象模型):windowlocationhistory
    • DOM(文档对象模型):documentElementEvent
  • Node.js 环境
    • fs(文件系统模块)、http(网络模块)等。
      特点
  • 依赖宿主环境,不同环境下对象可能不同(如浏览器与 Node.js 的差异)。
  • 可能包含非标准方法或扩展功能。

四、三者的核心区别

维度本地对象内置对象宿主对象
定义来源ECMAScript 规范ECMAScript 规范宿主环境(如浏览器)
实例化方式需通过 new 创建直接使用(如 Math由环境自动提供
依赖关系独立于宿主环境独立于宿主环境完全依赖宿主环境
典型示例ArrayDateGlobalMathdocumentwindow

五、总结

  1. 本地对象是语言基础,内置对象是其子集,宿主对象是环境扩展。
  2. 内置对象(如 Math)和宿主对象(如 document)均无需显式实例化,但前者是语言规范,后者是环境特性。
  3. 开发中需注意宿主对象的兼容性问题(如不同浏览器的 DOM 实现差异)。

Js 中,有哪些方法可以退出循环

在 JavaScript 中,退出循环的方法因循环类型而异,以下是常见方法及适用场景的总结:


一、传统循环(forwhiledo...while

  1. break 语句

    • 作用:立即终止整个循环,后续代码和循环均不执行。
    • 适用场景forwhiledo...while 等标准循环结构。
    • 示例
    javascript
    for (let i = 0; i < 5; i++) {
      if (i === 3) break // 循环终止于 i=3
      console.log(i)
    }
    
  2. continue 语句

    • 作用:跳过当前迭代,直接进入下一次循环。
    • 示例
      javascript
      for (let i = 0; i < 5; i++) {
        if (i === 2) continue // 跳过 i=2
        console.log(i)
      }
      
  3. return 语句

    • 作用:在函数内部终止循环并退出函数。
    • 注意:仅适用于函数作用域,全局使用会报错。
    • 示例
      javascript
      function test() {
        for (let i = 0; i < 5; i++) {
          if (i === 3) return // 函数终止,循环结束
        }
      }
      

二、数组迭代方法(forEachmap 等)

  1. forEach 的限制
    • 问题forEach 无法通过 breakreturn 退出循环,会继续遍历所有元素。
    • 替代方案
    • 抛出异常:通过 try...catch 包裹循环,抛出错误强制终止。
      javascript
      try {
        ;[1, 2, 3].forEach(num => {
          if (num === 2) throw new Error('Break')
        })
      } catch (e) {} // 捕获异常实现退出
      
    • 改用 for...of:支持 break 语句,更灵活。
      javascript
      for (const num of [1, 2, 3]) {
        if (num === 2) break // 直接退出
      }
      

三、其他方法

  1. 修改循环条件

    • 通过改变循环变量或条件表达式间接终止循环(例如 while 循环中设置标志变量)。
    • 示例
    javascript
    let flag = true
    while (flag) {
      if (condition) flag = false // 终止循环
    }
    
  2. 数组方法 some()every()

    • some():当回调函数返回 true 时终止遍历。
    • every():当回调函数返回 false 时终止遍历。
    • 示例
      javascript
      ;[1, 2, 3].some(num => {
        if (num === 2) return true // 终止遍历
      })
      

四、总结与建议

方法/场景适用循环类型特点
breakforwhilefor...of直接终止循环
continue同上跳过当前迭代
return函数内的循环终止函数及循环
异常捕获forEachmap强制退出,但代码不够优雅
改用 for...of替代 forEach支持 break,推荐方案

注意:优先使用 for...of 替代 forEach 以增强流程控制灵活性。

什么是事件委托

事件委托 本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到
目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。

使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。

什么是事件循环

  • 事件循环是指 js 引擎执行 js 代码的机制,js 引擎首先会执行一个全局代码块,然后执行所有的同步代码,同步代码执行完毕后,js 引擎会去查找所有的异步事件,当异步事件有了结果后,js 引擎会将异步事件加到事件队列中,等主线程执行完毕后,js 引擎会去事件队列中读取事件,然后执行对应的异步事件

事件循环(Event Loop)是 JavaScript 中一个重要的运行机制,它允许 JavaScript 引擎在单线程环境中处理异步操作。事件循环使得 JavaScript 能够同时处理高 IO 操作和用户交互,而不会因为单个任务的长时间运行而阻塞整个程序。

以下是事件循环的基本工作原理:

  1. 调用栈(Call Stack)

    • JavaScript 代码执行时,所有同步任务都在调用栈中执行。当一个函数执行时,它会被添加到栈顶,执行完成后从栈顶移除。
  2. 事件队列(Event Queue)

    • 异步任务(如定时事件、网络请求、用户交互等)完成后,它们的回调函数会被放入事件队列中等待执行。
  3. 事件循环(Event Loop)

    • 事件循环是 JavaScript 运行时的心跳,它不断检查调用栈是否为空。
    • 当调用栈为空时,事件循环会从事件队列中取出第一个任务,放入调用栈中执行。
    • 这个过程会不断重复,即循环检查调用栈和事件队列。
  4. 宏任务(Macro Tasks)和微任务(Micro Tasks)

    • 宏任务包括:setTimeout、setInterval、I/O、UI 渲染等。
    • 微任务包括:Promise.then、MutationObserver、queueMicrotask 等。
    • 当调用栈清空后,事件循环首先处理所有微任务队列中的任务,然后才处理下一个宏任务。
  5. 执行顺序

    • 事件循环的执行顺序是:同步任务 → 微任务 → 宏任务。
    • 当一个宏任务执行时,它的回调函数会被放入宏任务队列,等待下一个事件循环周期执行。
    • 微任务会在当前宏任务结束后立即执行,这意味着它们具有比下一个宏任务更高的优先级。

事件循环是 JavaScript 非阻塞 I/O 模型的核心,它使得 JavaScript 能够在不牺牲性能的情况下处理大量异步事件。

什么是闭包

  • 闭包是指有权访问另一个函数作用域中变量的函数
  • 闭包可以用来在函数外部访问函数内部的变量
  • 闭包可以用来封装变量,防止变量被其他代码修改

说说你对高阶函数的理解

高阶函数(Higher-Order Function)是函数式编程的核心概念,在 JavaScript 中具有重要地位。其核心特征和意义如下:


一、高阶函数的定义

高阶函数需满足以下任一条件

  1. 接收函数作为参数
    (例如:Array.map(callback)
  2. 返回一个新函数
    (例如:函数柯里化、闭包工厂)

二、典型应用场景

1. 抽象通用逻辑

javascript
// 普通函数:重复逻辑
function processArray(arr) {
  const result = []
  for (let item of arr) {
    result.push(item * 2) // 硬编码操作
  }
  return result
}

// 高阶函数:解耦操作
function map(arr, transform) {
  const result = []
  for (let item of arr) {
    result.push(transform(item)) // 操作由外部传入
  }
  return result
}
// 使用:map([1,2,3], x => x * 2)

优势:将数据遍历与具体操作解耦,提升代码复用性。


2. 延迟执行与组合

javascript
// 返回函数的场景:权限校验高阶函数
function withAuthCheck(Component) {
  return function (props) {
    if (!isLoggedIn()) {
      return <LoginPage />
    }
    return <Component {...props} />
  }
}
// 使用:const ProtectedProfile = withAuthCheck(Profile);

优势:通过函数组合实现逻辑复用(类似 React 高阶组件)。


3. 控制副作用

javascript
// 高阶函数封装异步错误处理
function catchAsyncError(fn) {
  return function (...args) {
    return fn(...args).catch(err => {
      console.error('Async error:', err)
      throw err
    })
  }
}
// 使用:const safeFetch = catchAsyncError(fetchData);

优势:统一处理异步流程中的异常,避免重复 try/catch。


三、与普通函数的对比

特性普通函数高阶函数
参数/返回值类型仅处理基础数据类型可处理函数类型
抽象层级实现具体业务逻辑抽象通用模式
复用性依赖具体实现通过参数配置不同行为
代码量通常更简短可能增加间接层

四、实际开发价值

  1. DRY 原则:避免重复代码(如统一处理 loading/error 状态)
  2. 声明式编程:用 arr.filter(x => x > 0) 替代手动循环
  3. 函数组合:通过 compose(f, g)(x) 实现管道操作
  4. 框架设计:Redux 中间件、Express 路由处理等核心机制依赖高阶函数

五、面试扩展方向

若需深入可补充:

  1. 闭包关联:高阶函数常利用闭包维持状态(如柯里化缓存参数)
  2. 性能考量:频繁创建函数可能影响内存,需权衡使用场景
  3. Monad 概念:高阶函数是理解函数式编程更高级概念的基础

高阶函数体现了 JavaScript 的「函数是一等公民」特性,是提升代码质量的关键工具。

doc 操作,怎么样添加、移动、复制、创建、删除、查找节点

  • 创建新节点
    • createDocumentFragment() // 创建一个 DOM 片段
    • createElement() // 创建一个具体的元素
    • createTextNode() // 创建一个文本节点
  • 添加、移除、替换、插入
    • appendChild()
    • removeChild()
    • replaceChild()
    • insertBefore() // 在已有的子节点前插入一个新的子节点
  • 查找
    • getElementsByTagName() // 通过标签名称
    • getElementsByName() // 通过元素的 Name 属性的值(IE 容错能力较强,会得到一个数组,其中包括 id 等于 name 的)
    • getElementById() // 通过元素 Id,唯一性

写一个通用的事件侦听器函数,为什么要用它?

  • 兼容性写法
js
function addEvent(element, type, handler) {
  if (element.addEventListener) {
    // DOM2.0 开始支持
    element.addEventListener(type, handler, false)
  } else if (element.attachEvent) {
    // IE 支持
    element.attachEvent('on' + type, handler)
  } else {
    // DOM 0
    element['on' + type] = handler
  }
}

underfined 和 null 的区别

  • null 表示一个对象是“没有值”,也就是值为“空”;
  • undefined 表示一个变量声明了没有赋值;

主要用于赋值给一些可能会返回对象的变量,作为初始化。

undefined 在 js 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它 会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。

当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等 号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

使用 Symbol 函数有哪一些注意的点?

1. 唯一性(核心特性)

  • 每个 Symbol 值都是唯一的,即使传入相同的描述参数:
    js
    const s1 = Symbol('foo')
    const s2 = Symbol('foo')
    console.log(s1 === s2) // false
    
  • 用途:适合作为对象属性的唯一标识符,避免命名冲突。

2. 不能使用new调用

  • Symbol 是原始类型(primitive),不是构造函数
    js
    const sym = new Symbol() // TypeError: Symbol is not a constructor
    

3. 描述参数(Description)

  • 创建 Symbol 时可传入字符串作为描述(仅用于调试,不影响唯一性):
    js
    const sym = Symbol('description')
    console.log(sym.description) // 'description'
    
  • 如果传入非字符串参数,会自动转换为字符串
    js
    Symbol({}) // Symbol([object Object])
    

4. 全局注册表(Global Registry)

  • 使用Symbol.for(key)在全局注册表中创建或复用 Symbol:
    js
    const s1 = Symbol.for('foo')
    const s2 = Symbol.for('foo')
    console.log(s1 === s2) // true
    
  • 通过Symbol.keyFor(sym)获取全局 Symbol 的键:
    js
    Symbol.keyFor(s1) // 'foo'
    

5. 类型转换限制

  • Symbol不能隐式转换为字符串或数值,否则会报错:
    js
    const sym = Symbol()
    console.log(sym + '') // TypeError
    console.log(Number(sym)) // TypeError
    
  • 但可以显式转换为字符串
    js
    String(sym) // 'Symbol()'
    sym.toString() // 'Symbol()'
    

6. 作为对象属性时的特性

  • Symbol 属性不会被常规方法遍历(如for...inObject.keys()):
    js
    const obj = { [Symbol('key')]: 'value' }
    console.log(Object.keys(obj)) // []
    
  • 需用Object.getOwnPropertySymbols()Reflect.ownKeys()获取:
    js
    Object.getOwnPropertySymbols(obj) // [Symbol(key)]
    Reflect.ownKeys(obj) // [Symbol(key)]
    

7. 内置 Symbol 值(Well-known Symbols)

  • 用于定义对象的内置行为(如迭代器、toString 标签等):
    js
    const obj = {
      [Symbol.iterator]: function* () {
        yield 1
      },
    }
    console.log([...obj]) // [1]
    
  • 其他内置 Symbol:Symbol.toStringTagSymbol.hasInstance等。

8. JSON 序列化

  • Symbol 属性会被JSON.stringify()忽略
    js
    const obj = { [Symbol('key')]: 'value' }
    JSON.stringify(obj) // '{}'
    

9. 类型判断

  • 使用typeof检测 Symbol 类型:
    js
    typeof Symbol() // 'symbol'
    
  • 注意:instanceof不适用,因为 Symbol 是原始值。

10. 实际应用场景

  • 避免属性冲突:在库或框架中定义内部属性。
  • 模拟私有属性(结合闭包或 Proxy 实现)。
  • 定义元编程行为(如Symbol.iterator实现可迭代对象)。

  • 使用 Symbol 时需要注意它的唯一性核心特性,每个 Symbol 值都是独一无二的。
  • 创建时不能使用 new,描述参数仅用于调试。如果需要复用 Symbol,可以通过 Symbol.for()在全局注册表中管理。
  • Symbol 作为属性名时不会被常规方法遍历,需用专用 API 获取。此外,Symbol 不能隐式转换类型,但可以显式转为字符串。
  • 内置 Symbol 值(如 Symbol.iterator)常用于元编程,而 JSON 序列化会忽略 Symbol 属性。

JavaScript 的作用域和作用域链

  • 作用域:作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。
  • 作用域链:作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和 函数。

作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。

作用域链的创建过程跟执行上下文的建立有关

JavaScript 继承的几种实现方式?

  • 原型链继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。 例如:
javascript
function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType() //继承了SuperType
SubType.prototype.getSubValue = function () {
  return this.subproperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) //true
  • 借用构造函数继承,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

    例如:

javascript
function SuperType() {
  this.colors = ['red', 'blue', 'green']
}
function SubType() {
  //继承了SuperType
  SuperType.call(this)
}
var instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) //"red,blue,green,black"
var instance2 = new SubType()
console.log(instance2.colors) //"red,blue,green"
  • 组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。 例如:
javascript
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, age) {
  //继承属性
  SuperType.call(this, name)
  this.age = age
}
  • 原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。 例如:
javascript
var person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van'],
}
var anotherPerson = Object.create(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')
var yetAnotherPerson = Object.create(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Barbie')
console.log(person.friends) //"Shelby,Court,Van,Rob,Barbie"
  • 寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点是没有办法实现函数的复用。 例如:
javascript
function createAnother(original) {
  var clone = object(original) //通过调用函数创建一个新对象
  clone.sayHi = function () {
    //以某种方式来增强这个对象
    console.log('hi')
  }
  return clone //返回这个对象
}
  • 寄生组合式继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。 例如:
javascript
function inheritPrototype(subType, superType) {
  var prototype = object(superType.prototype) //创建对象
  prototype.constructor = subType //增强对象
  subType.prototype = prototype //指定对象
}

谈谈你对 this、call、apply 和 bind 的理解

  • this 总是指向函数的直接调用者(而非间接调用者);
  • 如果有 new 关键字,this 指向 new 出来的那个对象;
  • 在事件中,this 指向触发这个事件的对象,特殊的是,IE 中的 attachEvent 中的 this 总是指向全局对象 Window;
  • 箭头函数中的 this 总是指向函数定义时的 this,而非执行时。
  • call、apply、bind 中的 this 被强绑定在指定的那个对象上;

JavaScript 原型,原型链?有什么特点?

  • 每个对象都会在其内部初始化一个属性,就是 prototype (原型),当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去 prototype 里找这个属性,这个 prototype 又会有自己的 prototype,于是就这样一直找下去,也就是我们平时所说的原型链的概念。
  • 关系:instance.constructor.prototype = instance.proto
  • 特点:
    • JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关联的对象也会继承这一改变。
    • 原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问在父类型中定义的方法。
    • 原型链的问题:
      • 原型中包含的引用值会在所有实例间共享;
      • 无法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

解释一下原型、构造函数、实例、原型链 之间的关系?

在 JavaScript 中,构造函数、原型、实例、原型链四者构成了面向对象编程的核心机制。它们的关系可通过以下逻辑分层解释:


一、构造函数与原型

  1. 构造函数:通过 new 关键字创建对象的函数。例如:

    javascript
    function Person(name) {
      this.name = name
    }
    

    构造函数内部通过 this 定义实例属性。

  2. 原型(prototype):每个构造函数自动关联一个 prototype 属性,指向其原型对象。原型对象默认包含 constructor 属性,指回构造函数本身,形成双向引用:

    javascript
    Person.prototype.constructor === Person // true
    

    作用:原型对象存储公共方法或属性,供所有实例共享,避免重复占用内存。


二、实例与原型的关系

  1. 实例的创建:通过 new 调用构造函数生成实例:
    javascript
    const person1 = new Person('Alice')
    
  2. 实例与原型链的链接:实例通过内部属性 __proto__(或 Object.getPrototypeOf())访问构造函数的原型对象:
    javascript
    person1.__proto__ === Person.prototype // true
    
    访问机制:当实例访问属性时,优先查找自身属性;若未找到,则沿 __proto__ 向上查找原型对象中的属性。

三、原型链的形成

  1. 原型链的定义:通过让一个构造函数的原型对象指向另一个构造函数的实例,形成层级链式结构。例如:

    javascript
    function Student() {}
    Student.prototype = new Person() // Student 的原型链继承 Person
    
    • Student.prototypePerson 的实例,因此 Student.prototype.__proto__ 指向 Person.prototype
    • 实例 student1 可访问 Student.prototypePerson.prototype 的属性和方法。
  2. 链式结构终点:原型链的顶端是 Object.prototype(所有对象的基类),其 __proto__null,表示查找终止。


四、关系的综合示意图

实例(person1) → Person.prototype → Object.prototype → null
↑               ↑
构造函数(Person)  原型链继承的层级

五、关键总结

  • 构造函数:创建实例的模板。
  • 原型:存储共享属性和方法的对象,减少内存消耗。
  • 实例:通过原型链继承属性和方法的对象。
  • 原型链:通过 __proto__ 链接的多层继承机制,是实现 JavaScript 继承的核心。

应用场景:原型链常用于实现对象继承。例如,Array 继承自 Object,因此数组实例可调用 Object.prototype.toString() 方法。

浏览器

三种事件模型是什么?

事件 是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型。

  • DOM0 级模型: 这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。这种方式是所有浏览器都兼容的。

  • IE 事件模型: 在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。

  • DOM2 级事件模型: 在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。

png、jpg、gif 这些图片格式解释一下,分别什么时候用,webp 呢?

  • png 是一种无损数据压缩位图文件格式。
  • jpg 是一种针对相片使用的一种失真压缩方法,有损的。
  • gif 是一种位图文件格式,以 8 位颜色深度,支持动画的位图格式。
  • webp 格式是谷歌在 2010 年推出的图片格式,压缩率只有 jpg 的 2/3,大小比 jpg 小了 40%。

跨域

跨域(Cross-Origin Resource Sharing,简称 CORS)是指在浏览器中,由于同源策略的限制,一个源(origin)的网页向另一个源的服务器发起 HTTP 请求时可能会遇到的问题。这里的“源”通常指的是协议(如 HTTP 或 HTTPS)、域名和端口的组合。

同源策略

同源策略是浏览器的一种安全机制,它限制了来自不同源的文档或脚本如何与当前文档交互。这是为了防止恶意网站通过脚本对用户数据进行未授权访问。

跨域问题

当一个网页尝试向与它不同源的服务器发起 HTTP 请求时,就会遇到跨域问题。例如,如果网页http://www.example.com尝试访问http://api.otherdomain.com的数据,由于它们源不同,浏览器会阻止这次请求,除非服务器明确允许这种跨域访问。

解决跨域的方法

  1. JSONP(JSON with Padding)

    • 通过动态创建<script>标签来绕过同源策略的限制,因为<script>标签可以加载跨域的 JavaScript 代码。
  2. CORS(Cross-Origin Resource Sharing)

    • 服务器端设置特定的 HTTP 响应头Access-Control-Allow-Origin,允许某些源的请求访问资源。
  3. 代理服务器

    • 在客户端和目标服务器之间设置一个代理服务器,客户端的请求先发送到代理服务器,然后由代理服务器转发到目标服务器。代理服务器返回的数据再由代理服务器转发回客户端。
  4. PostMessage

    • HTML5 引入的新特性,允许不同源的窗口、iframe 等进行通信。
  5. CORS Anywhere

    • 使用第三方服务作为代理,它会自动处理 CORS 问题。
  6. 文档域(document.domain)

    • 当两个页面具有相同的文档域设置时,它们可以相互访问,即使它们的 URL 不同。
  7. Window.postMessage

    • 允许跨文档通信,可以安全地实现跨源通信。

CORS HTTP 响应头

  • Access-Control-Allow-Origin:指定允许访问资源的源。
  • Access-Control-Allow-Methods:指定允许的 HTTP 方法(如 GET、POST 等)。
  • Access-Control-Allow-Headers:指定允许的 HTTP 请求头。
  • Access-Control-Allow-Credentials:是否允许发送 Cookie。
  • Access-Control-Max-Age:预检请求的结果可以被缓存的时间。

理解跨域问题和解决方法对于前端开发者来说非常重要,因为它们涉及到 Web 应用的安全性和数据交互。

CSS

谈谈 rem 和 em

rem

rem 是 CSS3 中引入的一个相对单位,全称是 "root em"。它相对于根元素(html 元素)的字体大小来计算。例如,如果根元素的字体大小是 16px,那么 1rem 就等于 16px。

em

em 是一个相对单位,它相对于其父元素的字体大小来计算。例如,如果一个元素的字体大小是 16px,那么它的 1em 就等于 16px。如果一个元素的字体大小是 2em,那么它的字体大小就是 32px。

区别

  • rem 是相对于根元素(html 元素)的字体大小,而 em 是相对于其父元素的字体大小。
  • rem 的值是固定的,而 em 的值是相对于父元素的字体大小,因此 em 的值会随着父元素的字体大小变化而变化。

使用场景

  • rem 常用于响应式布局,因为它可以随着根元素的字体大小变化而变化,从而实现不同屏幕尺寸下的自适应布局。
  • em 常用于嵌套元素,因为它可以相对于父元素的字体大小来设置字体大小,从而实现不同层级元素的字体大小比例。

优点

  • rem 和 em 都可以用来实现响应式布局,但是 rem 的值是固定的,因此可以更方便地计算和调试。
  • rem 和 em 都可以相对于父元素的字体大小来设置字体大小,从而实现不同层级元素的字体大小比例。

缺点

  • rem 的值是固定的,因此如果根元素的字体大小变化,那么所有使用 rem 的元素的大小都会变化,这可能会导致布局问题。
  • em 的值是相对于父元素的字体大小,因此如果父元素的字体大小变化,那么使用 em 的元素的大小也会变化,这可能会导致布局问题。

vw 和 vh

vw

vw 是 CSS3 中引入的一个相对单位,全称是 "viewport width"。它相对于视口(viewport)的宽度来计算。例如,如果视口的宽度是 1000px,那么 1vw 就等于 10px。

vh

vh 是 CSS3 中引入的一个相对单位,全称是 "viewport height"。它相对于视口(viewport)的高度来计算。例如,如果视口的高度是 1000px,那么 1vh 就等于 10px。

区别

  • vw 是相对于视口的宽度,而 vh 是相对于视口的高度。- vw 和 vh 的值都是固定的,不会随着视口大小的变化而变化。

css 选择器的优先级

CSS 选择器的优先级是由选择器的类型和数量决定的。优先级从高到低依次为:

  1. 内联样式(inline style):内联样式直接在 HTML 元素的 style 属性中定义,优先级最高。
  2. ID 选择器(ID selector):ID 选择器使用 # 符号定义,例如 #my-element,优先级次之。
  3. 类选择器(class selector)、属性选择器(attribute selector)和伪类(pseudo-class):类选择器使用 . 符号定义,例如 .my-class,属性选择器和伪类使用 [] 和 : 符号定义,例如 [type="text"] 和 :hover,优先级再次之。
  4. 元素选择器和伪元素(pseudo-element):元素选择器使用元素名称定义,例如 div,伪元素使用 :: 符号定义,例如 ::before 和 ::after,优先级再次之。
  5. 通配符选择器(universal selector):通配符选择器使用 _ 符号定义,例如 _,优先级最低。

需要注意的是,选择器的优先级是可以叠加的,例如:

css
#my-element .my-class {
  color: red;
}

这个选择器的优先级是 ID 选择器(#my-element)和类选择器(.my-class)的优先级之和,因此它的优先级比单个 ID 选择器或类选择器的优先级高。

伪类和伪元素的区别

伪类

伪类是用于选择元素的特定状态,例如鼠标悬停(:hover)、被访问过的链接(:visited)等。伪类以冒号(:)开头,例如:

css
a:hover {
  color: red;
}

这个选择器会选择所有鼠标悬停在其上的链接,并将其颜色设置为红色。

伪元素

伪元素是用于选择元素的特定部分,例如元素的首字母(::first-letter)、第一行(::first-line)等。伪元素以双冒号(::)开头,例如:

css
p::first-letter {
  font-size: 200%;
}

这个选择器会选择所有段落的第一个字母,并将其字体大小设置为 200%。

区别

伪类和伪元素的主要区别在于它们的选择范围:

  • 伪类用于选择元素的特定状态,例如鼠标悬停、被访问过的链接等。
  • 伪元素用于选择元素的特定部分,例如元素的首字母、第一行等。

另外,伪类和伪元素的选择器语法也有所不同:伪类以冒号(:)开头,伪元素以双冒号(::)开头。

上次更新于: