2025年01月10日浙江鑫越系统科技前端面试

目录

  1. vue2 和 vue3 的区别
  2. vue 怎么封装组件
  3. js 怎么把一个数组置空
  4. 怎么组件自己调用自己的组件
  5. v-bind:attribute 和 v-bind="{attribute}" 的区别
  6. var let const 的区别
  7. this 指向
  8. 作用域链
  9. 闭包
  10. 原型链
  11. 事件循环

1. vue2 和 vue3 的区别

Vue 2 和 Vue 3 在多个方面存在区别,以下从架构设计、语法与 API、性能、生态系统等方面进行详细介绍:

架构设计
  • 响应式系统
    • Vue 2 :基于 Object.defineProperty() 实现响应式。这种方式有一定局限性,例如无法检测对象属性的添加和删除,对于数组,部分方法(如通过索引修改元素)也不能触发响应式更新。
    • Vue 3:采用 Proxy 对象实现响应式系统。Proxy 可以劫持整个对象,并能拦截更多操作,解决了 Vue 2 中响应式的一些限制,能更好地检测对象属性的变化,包括属性的添加、删除以及数组元素的修改等。
  • 代码组织
    • Vue 2:主要使用选项式 API(Options API),将不同的逻辑(如数据、方法、生命周期钩子等)分散在不同的选项中,在处理复杂组件时,可能会导致代码碎片化,逻辑分散难以维护。
    • Vue 3:引入了组合式 API(Composition API),允许开发者根据逻辑关注点来组织代码,将相关的逻辑封装在一起,提高了代码的复用性和可维护性,尤其适合大型项目。
语法与 API
  • 组件定义
    • Vue 2 :使用 Vue.extend() 或单文件组件(SFC)来定义组件,通过 export default 导出一个包含各种选项的对象。
    • Vue 3 :仍然支持单文件组件,但在组合式 API 中,可以使用 <script setup> 语法糖来简化组件的定义,减少样板代码。
vue 复制代码
<!-- Vue 2 组件定义 -->
<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue 2!'
    };
  }
};
</script>

<!-- Vue 3 组件定义(<script setup>) -->
<template>
  <div>{{ message }}</div>
</template>

<script setup>
import { ref } from 'vue';
const message = ref('Hello, Vue 3!');
</script>
  • 生命周期钩子
    • Vue 2 :有 beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed 等生命周期钩子。
    • Vue 3 :部分钩子名称发生了变化,beforeDestroy 改为 beforeUnmountdestroyed 改为 unmounted,并且在组合式 API 中可以使用 onBeforeMountonMounted 等函数来注册生命周期钩子。
javascript 复制代码
// Vue 2 生命周期钩子
export default {
  created() {
    console.log('Vue 2: Component created');
  }
};

// Vue 3 组合式 API 生命周期钩子
import { onMounted } from 'vue';

export default {
  setup() {
    onMounted(() => {
      console.log('Vue 3: Component mounted');
    });
  }
};
  • 响应式数据定义
    • Vue 2 :在 data 选项中定义响应式数据,使用 this 来访问。
    • Vue 3 :使用 ref()reactive() 函数来创建响应式数据。ref() 用于创建单个值的响应式数据,reactive() 用于创建对象的响应式数据。
javascript 复制代码
// Vue 2 响应式数据定义
export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
};

// Vue 3 响应式数据定义
import { ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const increment = () => {
      count.value++;
    };
    return {
      count,
      increment
    };
  }
};
性能
  • 渲染性能
    • Vue 2:渲染器在更新 DOM 时,使用虚拟 DOM 进行比较和更新,在处理大型组件树时,可能会有一定的性能开销。
    • Vue 3:重写了渲染器,采用了静态提升、PatchFlag 等优化技术,减少了虚拟 DOM 的比较范围,提高了渲染性能,尤其是在处理大型组件和频繁更新的场景下表现更优。
  • 内存占用
    • Vue 2:由于响应式系统的实现方式,在创建大量响应式对象时,可能会占用较多的内存。
    • Vue 3:Proxy 实现的响应式系统在内存使用上更加高效,减少了不必要的内存开销。
生态系统
  • 插件兼容性
    • Vue 2:拥有丰富的插件生态系统,但部分插件可能需要进行适配才能在 Vue 3 中使用。
    • Vue 3:随着时间的推移,越来越多的插件开始支持 Vue 3,但在过渡期间,可能会面临一些插件兼容性问题。
  • 工具链支持
    • Vue 2:与之配套的工具链(如 Vue CLI)已经非常成熟。
    • Vue 3:官方推出了 Vite 作为构建工具,它具有更快的冷启动和热更新速度,更适合现代前端开发。

2. vue 怎么封装组件

在 Vue 里封装组件可以提升代码复用性与可维护性。下面为你详细介绍封装组件的步骤和示例。

步骤
  1. 创建组件文件 :在项目里创建一个新的 .vue 文件,此文件即为组件。
  2. 定义组件结构 :在 <template> 标签里定义组件的 HTML 结构。
  3. 编写组件逻辑 :在 <script> 标签里编写组件的 JavaScript 逻辑。
  4. 添加样式 :在 <style> 标签里添加组件的 CSS 样式。
  5. 使用组件:在需要使用该组件的地方引入并注册它。
示例

下面是一个简单的按钮组件封装示例:

1. 创建组件文件 ButtonComponent.vue
vue 复制代码
<template>
  <button :class="classes" @click="handleClick">
    {{ label }}
  </button>
</template>

<script>
export default {
  name: 'ButtonComponent',
  props: {
    label: {
      type: String,
      default: 'Click me'
    },
    primary: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    classes() {
      return {
        'bg-blue-500 text-white py-2 px-4 rounded': this.primary,
        'bg-gray-500 text-white py-2 px-4 rounded': !this.primary
      };
    }
  },
  methods: {
    handleClick() {
      this.$emit('click');
    }
  }
};
</script>

<style scoped>
button {
  cursor: pointer;
}
</style>
2. 使用组件
vue 复制代码
<template>
  <div>
    <ButtonComponent label="Primary Button" primary @click="onPrimaryClick" />
    <ButtonComponent label="Secondary Button" @click="onSecondaryClick" />
  </div>
</template>

<script>
import ButtonComponent from './ButtonComponent.vue';

export default {
  name: 'App',
  components: {
    ButtonComponent
  },
  methods: {
    onPrimaryClick() {
      console.log('Primary button clicked');
    },
    onSecondaryClick() {
      console.log('Secondary button clicked');
    }
  }
};
</script>

<style scoped>
/* 全局样式 */
</style>
解释
  • <template>:定义了组件的 HTML 结构,这里是一个按钮元素。
  • <script> :定义了组件的 JavaScript 逻辑,包含 props(接收外部传入的数据)、computed(计算属性)和 methods(方法)。
  • <style> :定义了组件的 CSS 样式,scoped 属性保证样式仅应用于当前组件。
  • 使用组件 :在需要使用组件的地方引入并注册它,然后在 <template> 中使用组件标签。

通过以上步骤,你就可以在 Vue 项目中封装并使用组件了。

3. js 怎么把一个数组置空

在 JavaScript 里,有多种方法能将数组置空,下面为你详细介绍常见的几种方式及其示例代码。

方法一:将数组的长度设为 0

通过把数组的 length 属性设置成 0,能够直接清空数组,此操作会移除数组里的所有元素。

javascript 复制代码
let arr = [1, 2, 3, 4, 5];
arr.length = 0;
console.log(arr); // 输出: []
方法二:重新赋值为空数组

直接把数组变量重新赋值为空数组 [],不过要留意这种方法会让原数组失去引用,若有其他变量也引用了原数组,这些变量不会受到影响。

javascript 复制代码
let arr = [1, 2, 3, 4, 5];
let anotherArr = arr;
arr = [];
console.log(arr); // 输出: []
console.log(anotherArr); // 输出: [1, 2, 3, 4, 5]
方法三:使用 splice 方法

splice 方法可用于从数组里添加或删除元素。当你把起始位置设为 0,删除数量设为数组的长度时,就能清空数组。

javascript 复制代码
let arr = [1, 2, 3, 4, 5];
arr.splice(0, arr.length);
console.log(arr); // 输出: []

综上所述,若你想直接清空原数组且让所有引用该数组的变量都变为空数组,推荐使用将 length 属性设为 0 或者 splice 方法;若你仅想让当前变量指向一个新的空数组,可使用重新赋值为空数组的方法。

4. 怎么组件自己调用自己的组件

在前端开发里,组件自己调用自己也就是实现组件的递归调用,这种方式常用于处理树形结构的数据,像菜单、文件目录这类场景。下面分别以 Vue 和 React 为例,介绍如何实现组件的递归调用。

Vue 中实现组件递归调用

在 Vue 里实现组件递归调用,需确保组件自身可以在模板里引用自己。以下是一个简单的树形菜单组件示例:

vue 复制代码
<template>
  <ul>
    <li v-for="item in menuItems" :key="item.id">
      {{ item.label }}
      <!-- 若存在子菜单,递归调用组件 -->
      <TreeMenu v-if="item.children && item.children.length > 0" :menuItems="item.children" />
    </li>
  </ul>
</template>

<script>
export default {
  name: 'TreeMenu',
  props: {
    menuItems: {
      type: Array,
      required: true
    }
  }
};
</script>

<style scoped>
ul {
  list-style-type: none;
  padding-left: 20px;
}
</style>    
vue 复制代码
<template>
  <div id="app">
    <TreeMenu :menuItems="menuData" />
  </div>
</template>

<script>
import TreeMenu from './TreeMenu.vue';

export default {
  name: 'App',
  components: {
    TreeMenu
  },
  data() {
    return {
      menuData: [
        {
          id: 1,
          label: 'Menu Item 1',
          children: [
            {
              id: 2,
              label: 'Submenu Item 1',
              children: [
                {
                  id: 3,
                  label: 'Sub - submenu Item 1'
                }
              ]
            }
          ]
        },
        {
          id: 4,
          label: 'Menu Item 2'
        }
      ]
    };
  }
};
</script>    
React 中实现组件递归调用

在 React 里实现组件递归调用,同样是在组件的渲染函数中调用自身。以下是一个对应的树形菜单组件示例:

jsx 复制代码
import React from 'react';

const TreeMenu = ({ menuItems }) => {
  return (
    <ul>
      {menuItems.map((item) => (
        <li key={item.id}>
          {item.label}
          {/* 若存在子菜单,递归调用组件 */}
          {item.children && item.children.length > 0 && (
            <TreeMenu menuItems={item.children} />
          )}
        </li>
      ))}
    </ul>
  );
};

export default TreeMenu;    
jsx 复制代码
import React from 'react';
import TreeMenu from './TreeMenu';

const App = () => {
  const menuData = [
    {
      id: 1,
      label: 'Menu Item 1',
      children: [
        {
          id: 2,
          label: 'Submenu Item 1',
          children: [
            {
              id: 3,
              label: 'Sub - submenu Item 1'
            }
          ]
        }
      ]
    },
    {
      id: 4,
      label: 'Menu Item 2'
    }
  ];

  return (
    <div>
      <TreeMenu menuItems={menuData} />
    </div>
  );
};

export default App;    

上述代码展示了在 Vue 和 React 中实现组件递归调用的方法,你可以依据实际需求对代码进行修改和扩展。

5. v-bind:attribute 和 v-bind="{attribute}" 的区别

在Vue.js里,v-bind:attributev-bind="{attribute}" 这两种语法存在明显差异,下面为你详细介绍:

1. v-bind:attribute(简写为 :attribute

这种语法用于把单个表达式的值绑定到某个特定的属性上。就像下面这样:

vue 复制代码
<template>
  <div>
    <!-- 把isActive变量的值绑定到disabled属性 -->
    <button :disabled="isActive">提交</button>
    
    <!-- 把title变量的值绑定到title属性 -->
    <img :src="imageUrl" :title="imageTitle" />
  </div>
</template>
  • 要是isActive的值为true,按钮就会被禁用。
  • imageUrlimageTitle分别对应图片的srctitle属性。
2. v-bind="{attribute}"(对象语法)

该语法会对一个对象进行解构操作,然后把对象里的所有属性都绑定到元素上。示例如下:

vue 复制代码
<template>
  <div>
    <!-- 假设userData = { name: 'John', age: 30 } -->
    <UserProfile v-bind="userData" />
    
    <!-- 等同于 -->
    <UserProfile :name="userData.name" :age="userData.age" />
  </div>
</template>
  • 当使用v-bind="userData"时,userData对象中的所有属性(像nameage)都会被当作props传递给UserProfile组件。
  • 这一语法常用于批量传递props或者动态绑定多个属性。
主要区别
特性 v-bind:attribute v-bind="{...}"
绑定数量 一次只能绑定一个属性 可以同时绑定多个属性
表达式类型 支持任意类型的表达式 要求必须是对象类型
应用场景 适用于绑定单个属性 适合批量绑定多个属性
实际应用示例
vue 复制代码
<template>
  <div>
    <!-- 情况1:绑定单个属性 -->
    <input :value="message" />
    
    <!-- 情况2:批量绑定多个属性 -->
    <input v-bind="inputConfig" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello',
      inputConfig: {
        type: 'text',
        placeholder: '请输入内容',
        disabled: false
      }
    }
  }
}
</script>
总结
  • 若你需要绑定单个属性,就使用:attribute="expression"
  • 若你要批量绑定多个属性,建议使用v-bind="object"

合理运用这两种语法,能够让你的Vue代码变得更加简洁和具有可读性。

6. var let const 的区别

在 JavaScript 中,varletconst 是用于声明变量的关键字,它们的主要区别体现在作用域、变量提升、可变性和暂时性死区等方面。以下是详细对比:

1. 作用域规则
  • var :函数作用域(或全局作用域)
    • 在函数内部声明的变量只能在函数内部访问,在全局作用域声明的变量会成为全局对象(如浏览器中的 window)的属性。
  • letconst :块级作用域({} 内有效)
    • ifforwhile 等代码块中声明的变量,外部无法访问。

示例对比

javascript 复制代码
function testScope() {
  if (true) {
    var x = 10;    // 函数作用域
    let y = 20;    // 块级作用域
    const z = 30;  // 块级作用域
  }
  console.log(x);  // ✅ 输出 10
  console.log(y);  // ❌ ReferenceError
  console.log(z);  // ❌ ReferenceError
}
2. 变量提升(Hoisting)
  • var :存在变量提升,可在声明前访问(值为 undefined
  • letconst:存在暂时性死区(TDZ),声明前访问会报错

示例对比

javascript 复制代码
console.log(a);  // ✅ undefined(var 提升但未赋值)
console.log(b);  // ❌ ReferenceError(TDZ)
console.log(c);  // ❌ ReferenceError(TDZ)

var a = 1;
let b = 2;
const c = 3;
3. 可变性
  • varlet:变量值可修改
  • const:常量值不可修改(必须在声明时赋值,且不能重新赋值)

示例对比

javascript 复制代码
var a = 1;
a = 2;  // ✅ 允许修改

let b = 3;
b = 4;  // ✅ 允许修改

const c = 5;
c = 6;  // ❌ TypeError(不能重新赋值)

// 注意:const 声明对象/数组时,对象属性或数组元素可修改
const obj = { name: 'Alice' };
obj.name = 'Bob';  // ✅ 允许修改属性
obj = {};          // ❌ TypeError(不能重新赋值对象)
4. 重复声明
  • var:允许在同一作用域重复声明同名变量(后面的会覆盖前面的)
  • letconst:不允许在同一作用域重复声明同名变量

示例对比

javascript 复制代码
var a = 1;
var a = 2;  // ✅ 允许,a 变为 2

let b = 3;
let b = 4;  // ❌ SyntaxError(重复声明)

const c = 5;
const c = 6;  // ❌ SyntaxError(重复声明)
5. 全局作用域行为
  • var:在全局作用域声明的变量会成为全局对象的属性
  • letconst:在全局作用域声明的变量不会成为全局对象的属性

示例对比

javascript 复制代码
var x = 10;
console.log(window.x);  // ✅ 输出 10

let y = 20;
const z = 30;
console.log(window.y);  // ❌ undefined
console.log(window.z);  // ❌ undefined
推荐使用场景
  • let:需要重新赋值的变量,尤其是在块级作用域中(如循环、条件语句)。
  • const:不需要重新赋值的变量(默认优先使用),如常量、对象、函数引用等。
  • var:尽量避免使用,除非需要兼容旧代码或特殊场景(如函数作用域)。
总结表格
特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升 存在(值为 undefined 存在(TDZ) 存在(TDZ)
可变性 可变 可变 不可变(常量)
重复声明 允许 不允许 不允许
全局对象属性

通过合理使用 letconst,可以减少变量污染和提升代码的健壮性,这也是现代 JavaScript 开发的最佳实践。

7. this 指向

在 JavaScript 中,this 关键字的指向是动态的,它取决于函数的调用方式。这一点与其他语言有很大不同,也是 JavaScript 的一个难点。下面将详细介绍 this 的指向规则及其应用场景。

1. 全局作用域中的 this

在全局作用域中,this 指向全局对象(在浏览器中是 window 对象)。

javascript 复制代码
console.log(this === window); // true(在浏览器环境中)

var globalVar = 'global';
console.log(this.globalVar);  // 'global'(全局变量是全局对象的属性)

// 严格模式下,全局作用域中的 this 仍为全局对象
function test() {
  'use strict';
  console.log(this === window); // true
}
test();
2. 函数调用中的 this

普通函数调用时,this 指向全局对象(非严格模式)或 undefined(严格模式)。

javascript 复制代码
function showThis() {
  console.log(this);
}

showThis(); // window(非严格模式)或 undefined(严格模式)

// 严格模式示例
function strictThis() {
  'use strict';
  console.log(this); // undefined
}
strictThis();
3. 方法调用中的 this

当函数作为对象的方法调用时,this 指向调用该方法的对象。

javascript 复制代码
const person = {
  name: 'Alice',
  greet() {
    console.log(`Hello, ${this.name}`);
  }
};

person.greet(); // 'Hello, Alice'(this 指向 person 对象)

// 嵌套对象示例
const obj = {
  outer: {
    inner: {
      method() {
        console.log(this); // 指向 inner 对象
      }
    }
  }
};

obj.outer.inner.method(); // 输出 inner 对象
4. 构造函数中的 this

使用 new 调用构造函数时,this 指向新创建的实例对象。

javascript 复制代码
function Car(color) {
  this.color = color;
  this.showColor = function() {
    console.log(this.color);
  };
}

const redCar = new Car('red');
redCar.showColor(); // 'red'(this 指向 redCar 实例)
5. 箭头函数中的 this

箭头函数不绑定自己的 this,而是捕获其所在上下文的 this 值。

javascript 复制代码
const obj = {
  name: 'Bob',
  regular() {
    console.log(this.name); // 'Bob'
  },
  arrow: () => {
    console.log(this.name); // undefined(箭头函数的 this 继承自全局作用域)
  }
};

obj.regular(); // 'Bob'
obj.arrow();   // undefined

// 常见应用:在回调函数中保持 this 指向
const timer = {
  seconds: 10,
  start() {
    setInterval(() => {
      this.seconds--; // 箭头函数的 this 指向 timer 对象
      console.log(this.seconds);
    }, 1000);
  }
};

timer.start();
6. callapplybind 方法

这三个方法可以显式地绑定函数的 this 值。

javascript 复制代码
function greet(message) {
  console.log(`${message}, ${this.name}`);
}

const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

// call():直接调用并指定 this
greet.call(person1, 'Hi'); // 'Hi, Alice'
greet.call(person2, 'Hello'); // 'Hello, Bob'

// apply():类似 call,但参数以数组形式传递
greet.apply(person1, ['Hi']); // 'Hi, Alice'

// bind():创建一个新函数,永久绑定 this
const greetAlice = greet.bind(person1);
greetAlice('Hey'); // 'Hey, Alice'
7. DOM 事件处理中的 this

在 DOM 事件处理函数中,this 通常指向触发事件的元素。

html 复制代码
<button id="myButton">Click me</button>
<script>
  const button = document.getElementById('myButton');
  button.addEventListener('click', function() {
    this.style.backgroundColor = 'red'; // this 指向 button 元素
  });
</script>
8. 类中的 this

在 ES6 类中,this 的指向与构造函数和方法的调用方式一致。

javascript 复制代码
class Counter {
  constructor() {
    this.count = 0;
  }
  
  increment() {
    this.count++;
    console.log(this.count);
  }
}

const counter = new Counter();
counter.increment(); // 1(this 指向 counter 实例)
总结:this 指向的判断规则
  1. 全局作用域this 指向全局对象(如 window)。
  2. 函数调用 :非严格模式下指向全局对象,严格模式下为 undefined
  3. 方法调用this 指向调用该方法的对象。
  4. 构造函数this 指向新创建的实例对象。
  5. 箭头函数this 继承自父级上下文。
  6. 显式绑定 :使用 callapplybind 方法指定 this
  7. DOM 事件this 指向触发事件的元素。
常见问题与注意事项
  • 箭头函数与普通函数的区别 :箭头函数没有自己的 this,适合用于需要保留上下文的场景(如回调函数);普通函数的 this 取决于调用方式。

  • 丢失 this 绑定

    javascript 复制代码
    const obj = {
      name: 'Alice',
      greet() {
        console.log(`Hello, ${this.name}`);
      }
    };
    
    const greetFunc = obj.greet;
    greetFunc(); // 错误:this 指向全局对象或 undefined

    解决方法:使用 bind() 绑定 this,或使用箭头函数。

通过理解 this 的指向规则,你可以更准确地编写 JavaScript 代码,避免因 this 指向不明导致的错误。

9. 闭包

在 JavaScript 里,闭包是一个强大且重要的概念。下面为你详细解释 JavaScript 中的闭包。

定义

闭包是指有权访问另一个函数作用域中变量的函数。简单来说,即使外部函数执行完毕,其作用域内的变量也不会被销毁,而是会被闭包"捕获"并保留,使得这些变量能在外部函数之外被访问和修改。

形成条件

闭包的形成需要满足以下两个关键条件:

  1. 函数嵌套:必须存在一个外部函数和至少一个内部函数。
  2. 内部函数引用外部函数的变量:内部函数使用了外部函数作用域内的变量。
作用

闭包在 JavaScript 中有多种重要作用:

  • 读取函数内部的变量:外部函数执行结束后,其内部变量会被闭包保存,可通过闭包在外部访问这些变量。
  • 让这些变量的值始终保持在内存中:变量不会因外部函数执行完毕而被销毁,而是持续存在于内存里,方便后续使用。
  • 封装私有变量和方法:可以使用闭包来创建私有变量和方法,避免全局作用域的污染。
示例
javascript 复制代码
function outerFunction() {
  // 外部函数的变量
  let counter = 0;
  // 内部函数,形成闭包
  function innerFunction() {
    counter++;
    return counter;
  }
  return innerFunction;
}

// 创建闭包实例
const closure = outerFunction();

// 调用闭包
console.log(closure()); // 输出: 1
console.log(closure()); // 输出: 2
console.log(closure()); // 输出: 3

在这个示例中,outerFunction 是外部函数,innerFunction 是内部函数。innerFunction 引用了 outerFunction 作用域内的 counter 变量,从而形成了闭包。当 outerFunction 执行完毕后,counter 变量不会被销毁,而是被 innerFunction 捕获并保留。每次调用 closure 函数时,counter 变量的值都会增加。

闭包的潜在问题

虽然闭包功能强大,但也可能带来一些问题,比如内存泄漏。由于闭包会让变量一直存在于内存中,如果闭包使用不当,可能会导致内存占用过高。因此,在使用闭包时,需要注意内存的使用情况,避免不必要的内存消耗。

10. 原型链

原型链是JavaScript中实现继承和对象属性查找的一种机制。以下是关于原型链的详细介绍:

原型的概念

在JavaScript中,每个对象都有一个原型(prototype)。原型也是一个对象,它可以包含一些属性和方法。当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript引擎就会去它的原型对象中查找。

原型链的形成
  • 所有的对象都默认从 Object.prototype 继承属性和方法。例如,toString()valueOf() 等方法就是从 Object.prototype 继承来的。
  • 当创建一个函数时,JavaScript会自动为这个函数添加一个 prototype 属性,这个属性指向一个对象,称为该函数的原型对象。当使用构造函数创建一个新对象时,新对象的 __proto__ 属性(也称为原型链指针)会指向构造函数的原型对象。这样就形成了一条链,从新对象开始,通过 __proto__ 不断指向它的原型对象,直到 Object.prototype,这条链就是原型链。
原型链的作用
  • 实现继承 :通过原型链,一个对象可以继承另一个对象的属性和方法。例如,定义一个 Animal 构造函数,再定义一个 Dog 构造函数,让 Dog 的原型指向 Animal 的实例,这样 Dog 的实例就可以继承 Animal 的属性和方法。
  • 属性和方法的共享 :多个对象可以共享原型对象上的属性和方法,节省内存空间。比如,所有数组对象都共享 Array.prototype 上的 push()pop() 等方法。
示例代码
javascript 复制代码
// 定义一个构造函数
function Person(name) {
  this.name = name;
}

// 在构造函数的原型上添加方法
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

// 创建一个Person的实例
const person1 = new Person('John');

// 访问实例的属性和方法,先在实例本身查找,找不到就去原型上查找
person1.sayHello(); // 输出 "Hello, my name is John"
console.log(person1.__proto__ === Person.prototype); // 输出 true

在这个例子中,person1Person 构造函数的实例,它的 __proto__ 属性指向 Person.prototype。当调用 person1.sayHello() 时,由于 person1 本身没有 sayHello 方法,JavaScript会沿着原型链在 Person.prototype 上找到该方法并执行。

11. 事件循环

以下是对 JavaScript 事件循环的更深入解释:

基本概念
  • 单线程执行模型:JavaScript 是单线程的,即在同一时间内只能执行一个任务。这意味着 JavaScript 代码按顺序执行,不会出现多个任务同时执行的情况。但为了处理异步操作,JavaScript 引入了事件循环机制,使它可以在等待某些操作完成时继续执行其他代码。
核心组件
  • 执行栈(Call Stack)
    • 执行栈是一个后进先出(LIFO)的数据结构,用于存储当前正在执行的函数调用。
    • 当一个函数被调用时,它会被压入执行栈;当函数执行完成,它会从栈中弹出。
    • 例如:
javascript 复制代码
function first() {
    second();
}
function second() {
    third();
}
function third() {
    console.log('Hello, World!');
}
first();
复制代码
- 调用 `first()` 时,`first` 函数会被压入执行栈;`first` 函数调用 `second()`,`second` 函数会被压入执行栈;`second` 函数调用 `third()`,`third` 函数会被压入执行栈;`third` 函数执行并打印 `Hello, World!`,然后 `third` 函数从栈中弹出,接着 `second` 函数弹出,最后 `first` 函数弹出。
  • 任务队列(Task Queue)
    • 任务队列存储着等待执行的任务,主要是异步操作的回调函数。
    • 任务队列可以分为宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)。
宏任务与微任务
  • 宏任务(Macrotasks)

    • 常见的宏任务包括 setTimeoutsetIntervalsetImmediate(Node.js)、I/O 操作、UI 渲染等。
    • 宏任务的执行顺序是一个接一个的,即执行完一个宏任务后,才会开始执行下一个宏任务。
    • 例如,setTimeout 函数会将其回调函数添加到宏任务队列中,当达到设定的延迟时间后,该回调函数会等待被执行。
  • 微任务(Microtasks)

    • 常见的微任务包括 Promise.then()Promise.catch()process.nextTick(Node.js)、queueMicrotask 等。
    • 微任务的优先级高于宏任务。在当前宏任务执行结束后,会优先执行微任务队列中的所有微任务,直到微任务队列为空。
    • 例如,Promise.resolve().then() 会将其回调函数添加到微任务队列中,该回调函数会在当前宏任务完成后立即执行,而不是等待下一个宏任务。
事件循环的执行流程
  1. 检查执行栈是否为空。
    • 如果执行栈不为空,继续执行栈中的函数调用。
    • 如果执行栈为空,进入下一步。
  2. 检查微任务队列是否为空。
    • 如果微任务队列不为空,按顺序依次执行微任务队列中的任务,直到微任务队列为空。
    • 如果微任务队列也为空,进入下一步。
  3. 从宏任务队列中取出一个任务,将其添加到执行栈中并执行。
  4. 重复上述步骤。
示例代码及详细解释
javascript 复制代码
console.log('Start');

setTimeout(() => {
    console.log('Timeout 1');
    Promise.resolve().then(() => {
        console.log('Promise inside Timeout 1');
    });
}, 0);

Promise.resolve().then(() => {
    console.log('Promise 1');
    setTimeout(() => {
        console.log('Timeout inside Promise 1');
    }, 0);
});

console.log('End');
  • 代码执行顺序如下:
    1. 首先,console.log('Start') 是同步代码,直接执行,输出 Start
    2. setTimeout(() => {...}, 0) 是宏任务,其回调函数被添加到宏任务队列中。
    3. Promise.resolve().then(() => {...}) 是微任务,其回调函数被添加到微任务队列中。
    4. console.log('End') 是同步代码,直接执行,输出 End
    5. 此时执行栈为空,检查微任务队列,发现 Promise.resolve().then(() => {...}) 的回调函数,执行该微任务,输出 Promise 1,并将另一个 setTimeout 回调添加到宏任务队列。
    6. 微任务队列已空,从宏任务队列中取出 setTimeout(() => {...}) 的回调函数,执行该宏任务,输出 Timeout 1,同时将内部的 Promise.then() 微任务添加到微任务队列。
    7. 再次检查微任务队列,执行内部的 Promise.then() 微任务,输出 Promise inside Timeout 1
    8. 最后,执行之前添加到宏任务队列的 setTimeout(() => {...}) 回调函数,输出 Timeout inside Promise 1
事件循环的重要性和应用场景
  • 重要性

    • 事件循环使 JavaScript 能够高效处理异步操作,避免因等待某些操作(如网络请求、文件读取等)而阻塞代码执行,保证程序的流畅性。
    • 理解事件循环有助于避免一些常见的异步编程错误,如竞态条件、回调地狱等。
  • 应用场景

    • 网络请求 :当使用 fetchXMLHttpRequest 进行网络请求时,请求完成后的回调函数会被添加到任务队列中,等待执行。
    • 用户交互:点击事件、输入事件等用户交互的处理函数会被添加到任务队列中,在用户触发事件后等待执行。
    • 定时器操作 :使用 setTimeoutsetInterval 等定时器,其回调函数会在设定的时间后添加到任务队列中。

在面试中,可以这样回答:"JavaScript 事件循环是一种处理异步操作的机制,它基于单线程执行模型。核心组件包括执行栈和任务队列,任务队列又分为宏任务队列和微任务队列。宏任务如 setTimeoutsetInterval 等,微任务如 Promise.then() 等。事件循环的执行流程是先检查执行栈是否为空,若为空,检查微任务队列,若微任务队列不为空,执行微任务直到为空,再从宏任务队列取一个任务执行,不断重复这个过程。这一机制使 JavaScript 可以在等待异步操作时继续执行其他代码,避免阻塞,同时保证了执行顺序。例如在处理网络请求、用户交互和定时器操作等场景中,事件循环能确保这些异步操作的回调函数在适当的时间得到执行,同时避免因等待而影响程序的流畅性。"

通过这样的详细解释和示例,可以清晰地阐述 JavaScript 事件循环的概念、流程、重要性和应用场景,让面试官了解你对该知识点的深入理解和掌握程度。

相关推荐
Bl_a_ck6 分钟前
【React】Craco 简介
开发语言·前端·react.js·typescript·前端框架
chenyuhao20241 小时前
链表的面试题4之合并有序链表
数据结构·链表·面试·c#
augenstern4161 小时前
webpack重构优化
前端·webpack·重构
海拥✘1 小时前
CodeBuddy终极测评:中国版Cursor的开发革命(含安装指南+HTML游戏实战)
前端·游戏·html
寧笙(Lycode)2 小时前
React系列——HOC高阶组件的封装与使用
前端·react.js·前端框架
asqq82 小时前
CSS 中的 ::before 和 ::after 伪元素
前端·css
拖孩2 小时前
【Nova UI】十五、打造组件库之滚动条组件(上):滚动条组件的起步与进阶
前端·javascript·css·vue.js·ui组件库
Hejjon2 小时前
Vue2 elementUI 二次封装命令式表单弹框组件
前端·vue.js
gaosushexiangji3 小时前
应用探析|千眼狼PIV测量系统在职业病防治中的应用
大数据·人工智能·科技·数码相机
189228048613 小时前
NY182NY183美光固态颗粒NY186NY188
大数据·网络·科技