前端面试题大汇总


react 的边缘错误指的是什么?

React 的边缘错误是指在 React 组件的渲染过程中发生的错误,这些错误通常发生在组件的生命周期方法中,例如 componentDidMountcomponentDidUpdatecomponentWillUnmount。边缘错误可能会导致组件无法正常渲染或出现意外行为。

常见的 React 边缘错误包括:

  • 状态更新不当:在组件的生命周期方法中更新状态可能会导致边缘错误。例如,在 componentDidMount 方法中直接更新状态可能会导致无限循环。
  • 副作用在生命周期方法中执行:在组件的生命周期方法中执行副作用(例如,发起网络请求或更新 DOM)可能会导致边缘错误。副作用应该在 useEffect 钩子函数或类组件的 componentDidMountcomponentDidUpdate 方法中执行。
  • 使用未定义的变量或属性:在组件的生命周期方法中使用未定义的变量或属性可能会导致边缘错误。例如,如果组件的状态中没有某个属性,而在生命周期方法中尝试访问该属性,则会引发错误。
  • 引用已卸载的组件:如果在组件卸载后仍试图访问其状态或方法,可能会导致边缘错误。例如,在 componentWillUnmount 方法中发起网络请求可能会导致错误,因为组件在请求完成之前已被卸载。

为了避免 React 边缘错误,您应该遵循以下最佳实践:

  • 在组件的生命周期方法中谨慎更新状态。
  • 在生命周期方法中避免执行副作用。
  • 在使用变量或属性之前确保它们已定义。
  • 在组件卸载后不要引用它。

如果您遇到了 React 边缘错误,可以使用浏览器的控制台来调试错误。控制台会显示错误消息以及错误发生的位置。您还可以使用 React 开发工具来调试错误。React 开发工具可以显示组件的生命周期方法的执行顺序,以及组件的状态和属性的变化情况。

js event loop

JavaScript 的 Event Loop 是其执行机制的核心,让 JavaScript 能在单线程上实现非阻塞的异步操作。具体的运行过程如下:

  1. 任务队列:JavaScript 有一个主线程用于执行任务,并且维护一个任务队列。如果有一些异步的操作,比如 setTimeout,Promise,或者是 I/O 操作,当它们完成时,会把相应的回调函数放入任务队列中。
  2. Event Loop:主线程会在任务队列中持续循环(或者说轮询),如果发现有任务就去执行它,这就是所谓的 Event Loop。
  3. 宏任务与微任务:任务队列中的任务分为宏任务和微任务两种类型。宏任务包括 script(整体代码),setTimeout, setInterval, setImmediate(Node.js 环境),I/O, UI rendering 等;微任务包括 Promise, process.nextTick(Node.js环境)等。在一个宏任务执行完后,会查看是否有微任务队列,如果有,就会把微任务队列中的所有微任务都执行掉,然后再去执行下一个宏任务。

简单地说,Event Loop 就是主线程不断地从任务队列中取任务执行,如果遇到异步任务就把任务放入任务队列中,等待再次被主线程取出执行。而微任务会在每次宏任务执行结束后立即执行,保证了比 setTimeout 等宏任务更快的响应。

js 的闭包性能问题如何解决

JavaScript的闭包是一个非常强大的功能,但是如果不正确地使用,可能会导致内存泄漏等性能问题。以下是几种解决 JavaScript 闭包性能问题的方法:

  1. 合理使用闭包:首先,应合理使用闭包。避免不必要的闭包可以减少内存的使用。只有当需要访问外部作用域的变量时,才创建闭包。
  2. 释放外部引用:当闭包不再被使用时,应该释放它对外部变量的引用。这可以通过将闭包内的外部引用设置为 null 来实现。
function foo() {
    var some_resource = getResource();
    return function() {
        some_resource.doSomething();
        some_resource = null; // 释放引用,将外部引用设置为null
    }
}
  1. 避免在循环中创建闭包:循环中使用闭包可能会导致意料之外的问题,因为每次迭代都会创建一个新的闭包。可以将回调函数或闭包解析到循环外部,或者使用让作用域变化的方法,比如 let 关键字。
function foo(){
  var funcs = [];
  for(let i = 0; i<10; i++){ // 使用 let 关键字,每次循环都会有一个新的作用域
    funcs[i] = function(){
      return i;
    };
  }
  return funcs;
}
  1. 使用函数声明而非函数表达式:函数声明与函数表达式创建闭包的方式略有差异,函数声明的方式在某些情况下对性能影响较小。

首先,我们需要明确的是,函数声明和函数表达式的主要区别在于 hoisting(提升)行为,而不是闭包。他们两者都可以创建闭包。不过,由于提升行为的不同,函数声明和函数表达式在某些使用场景下可能对性能有影响。

例如,考虑以下代码,使用函数表达式创建闭包:

var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = function() {
    return i;
  };
}
console.log(funcs[0]());  // 结果是 3,而不是我们可能期待的 0
console.log(funcs[1]());  // 结果是 3
console.log(funcs[2]());  // 结果是 3

以上代码的输出并不是我们期望的0,1,2,而是3,3,3。这是因为当闭包funcs[i]被调用时,i的值已经变为3,因为在循环中,我们实际上创建了指向同一内存地址(即变量i)的闭包。因此,当这些闭包访问其外部作用域中的i时,他们其实是访问的同一个i,即循环结束后的i。

而如果我们使用函数声明和立即执行函数表达式(IIFE),就可以避免这个问题:

var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = (function(value) {
    return function() {
      return value;
    };
  })(i);
}
console.log(funcs[0]());  // 结果是 0
console.log(funcs[1]());  // 结果是 1
console.log(funcs[2]());  // 结果是 2

以上代码的输出就是我们期望的0,1,2。在此,我们使用了立即执行函数表达式(IIFE)为每一个闭包创建了其自身的作用域,以保存循环中的当前变量i的值。这样,每一个闭包就有了自己的value变量,这个变量的值在闭包创建时就被确定,不受外部作用域的i的影响。

因此,以上的例子并不能支持 "函数声明与函数表达式创建闭包的方式略有差异,函数声明的方式在某些情况下对性能影响较小",他们在创建闭包和性能上并没有本质的区别。他们的区别更多的是在变量提升和作用域上,而不是闭包和性能。且在现代JavaScript引擎中,大多数情况下你不用担心函数声明与函数表达式间的性能差异。

vue 的双向绑定原理

Vue的双向数据绑定基于一种叫做"数据劫持"的技术,主要应用的是ES5中的Object.defineProperty()方法。双向数据绑定指的是:数据改变会影响视图,视图改变也会反过来影响数据。

具体实现的步骤如下:

  1. 数据劫持:Vue 首先会对数据进行劫持,通过 Object.defineProperty() 将属性转化为 gettersetter,当数据发生变化时,就可以通知对应的订阅者(也称为观察者)。
  2. 观察者模式:Vue 有一套观察者模式的实现,包括 Observer(观察者)、Dep(依赖收集器)和 Watcher(订阅者)。当数据发生变化时,Observer 会触发对应 Dep 中的 Watcher 进行更新,Dep 就是用来收集这些订阅者的。
  3. 指令解析:Vue 提供了指令,如 v-model,当解析器解析到这些指令会创建相应的订阅者,然后将新值和旧值等交给订阅者处理,从而实现视图的更新。
  4. 订阅器 Watcher:对于 v-model 这样的双向数据绑定来说,当视图(即输入框)发生变化时,它还会触发 Watcher 更新数据,从而实现数据的改变。

以上就是 Vue 的双向数据绑定的基本原理。虽然 Vue 3.x 已经开始使用 Proxy 替代 Object.defineProperty(),但是这个原理并未发生改变。希望这个解答能帮你理解 Vue 的双向数据绑定原理。

js 原型链的一些基础概念原理简单说明下

JavaScript 是一种基于原型的编程语言,它使用原型链实现继承和属性查找。

  1. 原型对象(Prototype)
    在 JavaScript 中,每当我们创建一个函数,JavaScript 引擎都会为这个函数自动添加一个特殊的内部属性 [[Prototype]],通常我们可以通过函数的 prototype 属性来访问到这个原型对象。原型对象默认会有一个 constructor 属性指向其关联的构造函数。
  2. 原型链
    当创建一个新的对象时(如通过构造函数或者字面量等方式),这个对象内部会有一个 [[Prototype]] 特殊属性(通常可以通过 __proto__ 属性访问到),它指向创建这个对象的函数的原型对象(prototype)。这样就形成了所谓的 "原型链"。
  3. 属性查找
    当我们试图访问一个对象的某个属性时,JavaScript 会首先在这个对象本身上查找,如果未找到,那么 JavaScript 就会沿着这个对象的原型链向上查找,直至找到属性或者到达原型链的顶端(null)。
  4. 继承
    JavaScript 中对象的继承主要是通过原型链实现的,子构造函数的原型对象是父构造函数实例,所以子构造函数的实例可以继承父构造函数实例的属性。当我们通过 new 关键字创建新对象时, 新对象会继承其构造函数的 prototype 属性指向的对象的属性。

以上就是 JavaScript 原型和原型链的一些基本概念和工作原理。这些是 JavaScript 对象系统的基础,了解和把握好原型及原型链对于编写高质量 JavaScript 代码非常重要。希望这个解答对你有所帮助。

js 闭包的原理 使用场景

JavaScript中的闭包是一种非常重要的概念,它是指有权访问另一个函数作用域中的变量的函数,主要分为以下两部分:

  1. 闭包的创建:当一个函数(我们称之为外部函数)返回另一个函数(我们称之为内部函数)时,就创建了一个闭包。内部函数通常会访问外部函数中的变量,而当外部函数的执行上下文从栈中弹出之后,这些变量实际上还会存在,因为闭包引用着它们。
  2. 闭包的应用:闭包的主要使用场景包括:

    • 保持状态:闭包可以捕获外部函数的变量,并在函数执行时保持其状态。这使得闭包在事件处理、回调函数等场景中极具价值。
    • 模拟块级作用域:JavaScript 在 ES6 之前并没有块级作用域,可以使用闭包来模仿一个块级作用域。
    • 创建私有变量:在 JavaScript 中,只有函数能够创建作用域。通过闭包,我们可以创建私有变量,使得这些变量不能在函数的外部被直接访问。

以下是一段创建闭包的代码示例:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

在这个例子中,makeAdder 是一个函数工厂 —— 它创建出像 add5add10 这样的函数。这些函数能够保存和访问makeAdder的参数。

需要注意的是,虽然闭包非常强大,但过度使用闭包可能会导致内存占用过大,需要谨慎使用。

以下是我推荐的一些关于闭包的深入理解及使用场景的内容:

  1. 深入理解JavaScript闭包之闭包的使用场景
  2. [[ JavaScript ] 对闭包的理解和使用场景 - 腾讯云](https://cloud.tencent.com/developer/article/1811450)
  3. js闭包的6种应用场景

proxy 实现响应式的原理

在 Vue3 中,响应式系统的核心是基于 ES6 的 Proxy 实现的,其原理主要涉及以下几个步骤:

  1. 创建响应式对象:通过 Proxy 创建一个响应式对象,该对象是对原始对象的代理。在该响应式对象中拦截了一些操作,如属性读(get)、属性写(set)、属性删除(deleteProperty)等。
  2. 依赖收集:当我们首次读取某个对象的某个属性时,就会触发 Proxyget 操作。在 get 的处理函数中,我们可以把这个"依赖"(即响应式变化的代码)收集起来,通常存放在一个叫 "Dep" 的类的实例中。这个 "Dep" 类就像一个盒子,用来装所有的依赖。
  3. 响应变化:当我们修改某个对象的某个属性时,就会触发 Proxyset 操作。在 set 的处理函数中,我们可以找到这个属性对应的 "Dep",然后遍历 Dep 中所有的依赖,把它们重新执行一遍,这样就实现了响应式。

以下是一个简单的代码示例:

function reactive(obj) {
  const handler = {
    get(target, prop) {
      // 收集依赖
      Dep.target && dep.addDep(Dep.target)
      return Reflect.get(target, prop)
    },
    set(target, prop, value) {
      const result = Reflect.set(target, prop, value)
      // 触发响应
      dep.notify()
      return result
    }
  }
  return new Proxy(obj, handler)
}

// 依赖收集器
class Dep {
  constructor() {
    this.deps = new Set()
  }
  addDep(dep) {
    this.deps.add(dep)
  }
  notify() {
    this.deps.forEach(dep => dep())
  }
}

let dep = new Dep()
let product = reactive({ price: 5, quantity: 2 })

let totalValue = 0

let effect = () => {
  totalValue = product.price * product.quantity
}

// 当前执行函数作为依赖添加到Dep
Dep.target = effect
effect()
Dep.target = null

console.log(totalValue) // => 10
product.price = 20
console.log(totalValue) // => 40

以上代码创建了一个响应式的 product 对象,当我们改变 product 的 price 或 quantity 时,totalValue 就会自动更新。

更多的具体内容和使用例子,以及可能的深入问题,你可以参考以下链接进行查阅:

  1. 基于Proxy实现响应式数据
  2. 带你彻底搞懂Vue3的Proxy响应式原理!TypeScript从零撸一个简化版
  3. 通过Proxy和Reflect实现vue的响应式原理

Vue3 这个框架的最大特点是什么,跟react的区别

Vue3 这个框架的最大特点主要有以下几点:

  1. Vue3 使用了 Composition API,改变了 Vue2 中的选项API的写法,使得代码组织更清晰,并且更容易进行类型检查,提高了代码的质量和健壮性。
  2. Vue3 提供更为强大的响应式模型,基于 ES6 Proxy,可以监听动态新增的属性,并能监听数组索引和 length 属性的变化。
  3. Vue3 实现了对 Virtual DOM 的优化,通过编译时优化,模板更直观且运行效率更高。
  4. Vue3 改进了 Diff 算法,优化了静态节点,减少不必要的对比和渲染。
  5. Vue3 的模板可以不用单独的根节点进行包裹。

与 React 的主要区别:

  1. 学习曲线:Vue 通常被认为更易学易用,提供了较为简单的模板语法,而 React 更重视 JavaScript 的方式进行开发,对初学者可能存在一定的学习曲线。
  2. 状态管理:虽然都有对应的状态管理工具(Vue 的 Vuex,React 的 Redux),但他们的状态管理风格不同。React 对状态管理的控制较为严格,而 Vue 的状态管理相对较为宽松。
  3. 开发规模:React 更加强大,适合开发大型的复杂应用,有更为丰富的生态和社区支持。而 Vue 则比较适合开发小型和中型的应用,且在 API 设计和开发体验上更优。
  4. 响应式实现:Vue 使用的响应式数据模型使其可以在数据改变时自动更新视图,而 React 则采用状态更新再渲染的方式。

这些都是 Vue3 和 React 的主要特点及其区别,他们各有优劣,哪种更适合开发则需要根据实际的项目需求和团队技术栈来决定。

Vue3 使用了什么新的响应式模型?

Vue3 使用了全新的响应式模型,基于 ES6 的 Proxy 代理和 Reflect 反射机制来实现响应式。这种新的响应式模型的运作方式与 Vue2 的基于 Object.defineProperty 的模型有着显著的差别。

Vue3 的响应式模型具有以下几个特点:

  1. 支持动态属性:借助 Proxy 和 Reflect,Vue3 可以监听对象的动态新增属性,而在 Vue2 中,如果在创建实例之后添加新的响应式属性,则该属性不会被监听。
  2. 改进了对数组的响应式处理:Vue3 可以监听数组索引和 length 属性的变化,对数组操作进行响应式处理。
  3. 表现更接近原生行为:由于 Proxy 代理不修改对象本身,而是生成了新的 Proxy 实例来操作对象,因此在响应式处理上,Vue3 的行为更接近 JavaScript 的原生行为。
  4. 更好的性能:Vue3 中使用的 Proxy,性能上比 Vue2 中使用的 Object.defineProperty 有着很大的优势。
  5. 更精确的依赖收集和更新机制:Vue3 中采用了更精确的依赖收集和更新机制,尽量降低了不必要的视图更新,提高了渲染效率。

此外,Vue3 还引入了 reactiveref 两个 API 来创建响应式对象,并通过 computed API 实现计算属性的响应式。这些新的 API 设计使得我们在开发中能够更灵活地创建和管理响应式数据,提高了开发效率。

更多的详细内容和示例,你可以参考以下文章:

  1. 浅析Vue3 响应式系统
  2. Vue3响应式原理以及依赖模型解析

vue3 里面 computed 跟 watch 的区别

Vue 3 当中,computedwatch 是两个对属性的监视者,但他们的使用场景和工作方式有所不同。

  1. computed(计算属性)

    • computed 用于定义一个基于依赖进行计算的属性。只有当它的依赖发生改变时,才会重新计算新的值。
    • computed 返回的是一个响应式的 ref 对象。
    • 通常,在模板中展示一些经过处理的值时,会用到 computed。例如,如果我们要展示一个用户的全名,可能需要将其名字和姓氏拼接起来,就可以使用 computed
    • 计算属性是基于它们的依赖关系缓存的,只有在依赖性改变时才会重新求值。

例如:

const count = ref(0);
const plusOne = computed(() => count.value + 1);
  1. watch(侦听器)

    • watch 可以监视一个具有响应式的属性或者一个返回响应式对象的函数,并在他们改变时触发一个回调函数。
    • watch 不返回任何值。
    • watch 对应多个数据源会更加有用,并关注这些数据源发生的更改,例如执行异步操作或较大开销操作。

例如:

const count = ref(0);
watch(count, (newVal, oldVal) => {
  console.log(`count changed from ${oldVal} to ${newVal}`);
});

总结来说,computedwatch 都能实现对数据的监控功能,但是需要根据具体的场景来选择。如果你需要依赖其他属性进行计算,那么应该使用 computed;如果你想在数据改变时执行特定的操作,那么使用 watch 会更适合。

react useMemo 跟 React.memo 有什么区别

在React中,useMemo和React.memo都被用于优化组件渲染性能,但是他们被应用在不同的场景,并且他们的工作方式也是不同的。

  1. useMemo 是一个React Hook,用于避免在每次渲染时都进行昂贵的计算。useMemo会记住这个函数的上一次返回结果,当它的依赖项(第二个参数:数组中的值)没有发生变化,那么将直接使用上一次的计算结果,而不会重新执行函数。注意:React不保证useMemo中的函数一定不会被重新执行,所以如果函数的执行不会产生副作用,且执行性能损耗可以接受,那就应该避免使用useMemo。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  1. React.memo 是一个高阶组件,它类似于React.PureComponent但用于函数组件。纯函数是指如果props相同,那么就能保证每次渲染得到的结果也相同。React.memo对组件进行浅层对比,查看props是否改变,若未改变则复用之前的渲染结果,减少渲染次数,提高性能。如果props经常变化或者对比props产生的性能损耗大于渲染组件产生的性能损耗,那可能就不适合使用React.memo。
const MyComponent = React.memo(function MyComponent(props) {
  /* 渲染使用props的内容 */
});

区别是:useMemo用于避免重复执行昂贵的函数,通过记住上一次的执行结果。而React.memo则是避免在props不变时重新渲染组件,通过记住上一次渲染的结果。总的来说,他们的共同目标都是优化组件的性能,但是使用场景和方法稍有不同。

Vue 里面 nextTick 的作用是什么

在Vue中,nextTick是一个非常重要的方法,用于延迟执行一段代码,使其在下次DOM更新循环结束之后运行。让我们更深入了解一下nextTick的作用和原理。

Vue.js 的数据驱动视图的核心思想是当组件的状态(data)改变时,Vue会自动更新与状态相关的DOM。然而,这个DOM的更新并不是立即执行,而是异步执行的。也就是说,当你设置了数据,DOM不会立即更新,而是等到本次事件循环结束时进行统一的、批量的更新。

在某些情况下,我们可能需要访问到已经被更新的DOM,比如获取一个列表的数量进行计算或者操作一个已经被渲染出来的DOM元素。这个时候就需要使用nextTick方法了。

通过使用Vue.nextTick(callback),你可以指定一个回调函数,这个函数会在下一次DOM更新循环之后被执行。这样你就可以获取到更新后的DOM。

下面是一个典型的例子:

new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    changeMessage() {
      this.message = 'Message changed';
      this.$nextTick(() => {
        console.log(this.$el.textContent) // 'Message changed'
      })
    }
  }
});

这段代码中,我们先改变了 message 的值,然后希望在DOM更新后打印出当前DOM的内容。如果没有使用nextTick,DOM的更新会在 console.log 运行后才执行,所以通过console.log获取到的任然是旧的DOM。使用nextTick后,我们可以在回调函数中获取到已经更新的DOM。

所以,简单来说 Vue 的 nextTick 方法的作用就是用于延迟执行一段代码,让该代码在下次的DOM更新循环结束后执行。这对于需要在DOM更新后进行操作的代码来说非常有用。

声明:八零秘林|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 前端面试题大汇总


记忆碎片 · 精神拾荒