前端面试题(一)

290 阅读31分钟

最近总算迎来了本人第一次面试啊,为什么这么晚,奈何太菜,笔试一直不过。这次总的来说问题不难,面试官小姐姐人美声甜,还很温柔。就是没复习(笔试一直没过),导致这次又是惨败收场,呜呜呜呜~

Ps:内容为本人整合,非原创。

1.JavaScript

说说闭包的概念

闭包是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函

数访问外部函数的作用域。

闭包主要用在下面几个方面:

  1. 访问外部函数的变量:闭包允许内部函数访问外部函数的变量,即使外部函数已经执行完毕或不再处于活动状态。这样可以创建私有变量,避免全局命名空间的污染。

  2. 保持数据的持久性:由于闭包可以持有外部函数的变量和参数的引用,即使外部函数已经执行完毕,闭包仍然可以访问和使用这些值。这使得在函数之外访问和修改数据成为可能,实现了一种持久性的数据保存。

  3. 实现模块化和封装:通过使用闭包,可以创建具有私有变量和公共方法的模块化结构。私有变量只能通过内部函数进行访问,而公共方法可以在外部访问。这种封装性可以避免对外暴露不必要的信息,并提供对内部状态的控制和保护。

  4. 保存函数状态:闭包还可以用于保存函数的状态。通过在闭包内部存储变量,函数可以记住先前的操作和状态,并在以后的调用中使用这些值。这在某些特定的情况下非常有用,例如计数器、迭代器等。

需要注意的是,闭包可能会导致内存泄漏问题。由于闭包持有外部函数的变量和参数的引用,如果闭包本身不被释放,这些引用也不会被释放,会导致内存占用增加。因此,在使用闭包时应特别注意内存管理,确保在不再需要闭包时正确释放资源。

说说冒泡和捕获

DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。先捕获再冒泡。

事件捕获(event capturing): 当鼠标点击或者触发dom事件时(被触发dom事件的这个元素被叫作事件源),浏览器会从根节点 =>事件源(由外到内)进行事件传播。

事件冒泡(dubbed bubbling):当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到 window (注意这里传递的仅仅是事件,例如click、focus等等这些事件, 并不传递所绑定的事件函数。)

事件捕获与事件冒泡是比较类似的,最大的不同在于事件传播的方向。

event.stopPropagation(); // 阻止冒泡
event.preventDefault();  // 阻止默认行为

说说事件委托

事件委托也称为事件代理。就是利用事件冒泡,把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托就无法实现。

简而言之,当子元素触发事件时,通过冒泡将事件传递给父元素,父元素通过e.target属性获取到对应的子元素,处理事件。

优点:

可以减少事件处理程序的数量,提高性能,并且动态添加或删除子元素时无需重新绑定事件。

缺点:

事件委托基于冒泡,对于不冒泡的事件不支持。

层级过多时,冒泡过程中,可能会被某层阻止掉。建议就近委托

说说事件循环

事件循环(Event Loop)是 JavaScript 运行时(如浏览器或 Node.js)中用于处理异步事件的一种机制。它负责管理执行栈(执行上下文)和任务队列(消息队列),以确保代码按照正确的顺序执行。

事件循环的工作过程可以简述如下:

  1. 执行全局上下文:当 JavaScript 引擎开始执行脚本时,它首先会创建一个全局执行上下文,并将其推入执行栈中。全局上下文包含了全局变量、函数定义等。
  2. 执行同步任务:从执行栈中取出当前的执行上下文,并执行其中的同步代码。如果遇到异步任务,例如定时器或网络请求,会将这些异步任务交给对应的 Web API 处理,而不会阻塞主线程。
  3. 将异步任务添加到任务队列:当异步任务完成并准备好被处理时(如定时器到期、网络请求完成等),它们(分为宏任务和微任务两类)会被添加到任务队列中。
  4. 执行微任务:在同步任务执行完毕后,将检查微任务队列(也称为 Job Queue),并按顺序执行队列中的所有微任务。微任务通常包括 Promise 回调、MutationObserver 和一些 DOM 事件等。
  5. 执行下一个宏任务:如果执行栈为空且微任务队列为空,则事件循环将从宏任务队列中选择下一个任务进行执行。常见的宏任务包括用户交互(如点击事件)、定时器回调和网络请求回调等。
  6. 重复上述步骤:事件循环会不断重复上述过程,循环处理任务队列中的任务,直到没有待处理的任务。

需要注意的是,事件循环是单线程的,并且在任何给定时刻只能执行一个任务。异步任务通过事件循环和任务队列的机制实现了非阻塞的执行,使得 JavaScript 能够处理多个同时发生的异步操作。

这种事件循环的机制确保了 JavaScript 代码的有序执行,并避免了长时间的阻塞,从而保持了用户界面的响应性。

同步任务和异步任务

JS做的任务分为同步和异步两种,所谓 "异步",简单说就是一个任务不是连续完成的,先执行第一段,等做好了准备,再回过头执行第二段,第二段也被叫做回调;同步则是连贯完成的。

像读取文件、网络请求这种任务属于异步任务:花费时间很长,但中间的操作不需要JS引擎自己完成,它只用等别人准备好了,把数据给他,他再继续执行回调部分。如果没有特殊处理,JS引擎在执行异步任务时,应该是存在等待的,不去做任何其他事情。这样在执行异步任务时有大量的空闲时间被浪费。对于JS这种单线程语言来说,这种长时间的空闲等待是不可接受的。

宏任务和微任务

事件循环由宏任务和在执行宏任务期间产生的所有微任务组成。 完成当下的宏任务后,会立刻执行所有在此期间入队的微任务。

常见的宏任务有:script(整体代码)/setTimout/setInterval

常见的微任务有:Promise.then()/Promise.catch()/Promise.finally()/Object.observe/MutationObserver/async/await

谈谈对原型链的理解

原型链(Prototype Chain)是 JavaScript 中实现继承的一种机制。它是基于对象的,每个 JavaScript 对象都有一个原型(Prototype)属性,它指向另一个对象。当我们访问对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法或达到原型链的顶端(即 Object.prototype)。

在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”。

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal
// 请注意,__proto__ 与内部的 [[Prototype]] 不一样。__proto__ 是 [[Prototype]] 的 getter/setter。
// __proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型。

每个函数都有 "prototype" 属性,即使我们没有提供它。默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。

当我们使用诸如 new F() 这样的构造函数来创建一个新对象。那么 new 操作符会使用它为新对象设置 [[Prototype]],指向的是构造函数的prototype属性。

function Rabbit() {}
// 默认:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // 相当于rabbit.__proto__ == { constructor: Rabbit }

alert(rabbit.constructor == Rabbit); // true (from prototype) // 继承自 {constructor: Rabbit}

Snipaste_2023-11-10_21-06-45.png

new关键字的原理

new关键字的实现原理可以分为以下几个步骤:

  1. 创建一个新的空对象。
  2. 将新创建的对象的__proto__属性指向构造函数的prototype属性。
  3. 将构造函数的this关键字指向新创建的对象。
  4. 执行构造函数中的代码,给这个空对象添加属性和方法。
  5. 如果构造函数没有显式返回一个对象,则返回新创建的对象。

谈谈js的继承

继承实际上是基于原型链来实现的,通过让子类对象的__proto__指向父类对象的原型(或实例对象),实现子类能够调用父类的属性和方法。构造函数继承,实现了给实例子类的时候传参,通过call()bind()方法绑定子类调用父类的构造函数,此时可以实现父类构造函数的方法。再加上原型,可以同时继承parent.prototype上的属性和方法(组合继承)。通过对父类对象进行浅拷贝创建一个新对象,让他的原型指向父对象实现继承。

1.原型链继承

function Person() {
  this.name = 'Person'
}
Person.prototype.eating = function () {
  console.log(this.name + 'eating~')
}
// 子类:特有属性和方法
function Student() {
  this.sno = 111
}
const p = new Person()
Student.prototype = p // 让Student构造函数的prototype属性指向父的实列对象
Student.prototype.studying = function () {
  console.log(this.name + ' studying~')
}
const stu = new Student()
console.log(stu.name) // Person
stu.eating() // Person eating~
stu.studying() // Person studying~

Snipaste_2023-11-22_09-09-25.png

优点:

父类方法可以复用

缺点:

  1. 父类的所有引用属性(info)会被所有子类共享,更改一个子类的引用属性(父类上的属性),其他子类也会受影响
  2. 子类型实例不能给父类型构造函数传参

2.构造函数继承

构造函数继承是在子类构造函数中调用父类构造函数,可以在子类构造函数中使用call()apply()方法。

function Parent(name) {
    this.info = { name: name };
}
function Child(name) {
    //继承自Parent,并传参
    Parent.call(this, name);
    
     //实例属性
    this.age = 18
}

let child1 = new Child("yhd");
console.log(child1.info.name); // "yhd"
console.log(child1.age); // 18

let child2 = new Child("wxb");
console.log(child2.info.name); // "wxb"
console.log(child2.age); // 18
//---------------------------------------------------------------------------------------------
/*
			构造函数继承
优点:
	可以在子类构造函数中向父类传参数
	父类的引用属性不会被共享
缺点:
	子类不能访问父类原型上定义的方法(即不能访问Parent.prototype上定义的方法),因此所有方法属性都写在构造函数中,每次创建实例	都会初始化
*/

3.组合继承

组合继承将原型链继承和构造函数继承结合的一种方式。它使用原型链继承来继承方法,使用构造函数继承来继承属性。但这种方法会调用两次父类构造函数,可能会导致性能问题。

function Parent(name) {
   this.name = name
   this.colors = ["red", "blue", "yellow"]
}
Parent.prototype.sayName = function () {
   console.log(this.name);
}

function Child(name, age) {
   // 继承父类属性
   Parent.call(this, name)
   this.age = age;
}
// 继承父类方法
Child.prototype = new Parent();

Child.prototype.sayAge = function () {
   console.log(this.age);
}

let child1 = new Child("yhd", 19);
child1.colors.push("pink");
console.log(child1.colors); // ["red", "blue", "yellow", "pink"]
child1.sayAge(); // 19
child1.sayName(); // "yhd"

let child2 = new Child("wxb", 30);
console.log(child2.colors);  // ["red", "blue", "yellow"]
child2.sayAge(); // 30
child2.sayName(); // "wxb"


上面例子中,Parent构造函数定义了name,colors两个属性,接着又在他的原型上添加了个sayName()方法。Child构造函数内部调用了Parent构造函数,同时传入了name参数,同时Child.prototype也被赋值为Parent实例,然后又在他的原型上添加了个sayAge()方法。这样就可以创建 child1,child2两个实例,让这两个实例都有自己的属性,包括colors,同时还共享了父类的sayName方法

优点:

  1. 父类的方法可以复用
  2. 可以在Child构造函数中向Parent构造函数中传参
  3. 父类构造函数中的引用属性不会被共享

4.原型式继承

这种继承方式使用一个已有的对象作为模板来创建新对象,新对象可以继承模板对象的属性和方法。但需要注意的是,修改子对象的属性也会影响到模板对象。

对参数对象的一种浅复制。

function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun()
}

let person = {
  name: "yhd",
  age: 18,
  friends: ["jack", "tom", "rose"],
  sayName:function() {
    console.log(this.name);
  }
}

let person1 = objectCopy(person);// 实质上就是新创建一个对象,让他的__proto__属性指向已创建对象
person1.name = "wxb";
person1.friends.push("lily");
person1.sayName(); // wxb

let person2 = objectCopy(person);
person2.name = "gsr";
person2.friends.push("kobe");
person2.sayName(); // "gsr"

console.log(person.friends); // ["jack", "tom", "rose", "lily", "kobe"]
// ES5的Object.create()方法在只有第一个参数时,与这里的objectCopy()方法效果相同

5.寄生式继承

这种方式类似于原型式继承,但在创建新对象时可以添加额外的属性和方法,这些额外的属性和方法可以根据需要进行定制。

function createChild(parent) {
const child = Object.create(parent);
child.name = 'Child';
child.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
return child;
}

const parent = {
name: 'Parent'
};

const child = createChild(parent);
child.sayHello(); // 输出 "Hello, I'm Child"

6.类继承

ES6 引入了 class 关键字,使面向对象编程更加简洁。可以使用 extends 关键字实现类的继承,子类可以继承父类的属性和方法。

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

sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}

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

const child = new Child();
child.sayHello(); // 输出 "Hello, I'm Child"

谈谈for in,for of的区别

for of遍历键值对的值,for in 遍历键值对的键。(不得不说,只答到这一点没有分)

for...in可以用在可枚举的数据,如:

  • 对象,还会遍历原型链上的属性
  • 数组
  • 字符串

for...of用于可迭代的数据,如:

  • 数组
  • 字符串
  • Map
  • Set

我们使用 Object.getOwnPropertyDescriptors 方法获取指定对象所有的自有属性的属性描述符。

说说promise

promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。

因此Promise的有三种状态:

  • pending: 初始状态,new一个promise对象后,Promise((resolve, reject)=>{})函数立即执行,状态不改变。
  • fulfilled: 操作成功完成,调用resolve()之后
  • rejected:操作失败,调用reject()之后

Promise在成功执行后完成执行注册在自身的Promise.prototype.then函数,如果失败后会调用Promise.prototype.catch。他是链式调用的,Promise所有的方法调后都会返回一个新的Promise对象。

then方法和catch方法是Promise原型上的一个方法:then方法会返回一个Promise,最多可以接收两个参数,分别是Promise成功和失败情况的回调函数;catch方法也返回一个Promise,当我们的Promise被拒绝时(reject)调用catch里的回调函数(相当于.then方法的第二个参数)。

async/await

async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

async 它作为一个关键字放到函数前面,用于表示函数是一个异步函数,异步函数意味着该函数的执行不会阻塞后面代码的执行,总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。

关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。

typeof 和 instanceof

typeof只能准确判断原始数据类型和函数(函数其实是对象,并不属于另一种数据类型,但也能够使用 typeof 进行区分),无法精确判断出引用数据类型(统统返回 object)。

const type =  typeof '中国万岁'; // string
typeof 666; // number
typeof true; // boolean
typeof undefined; // undefined
typeof Symbol(); // symbol
typeof 1n; // bigint
typeof () => {}; // function

typeof []; // object
typeof {}; // object
typeof new String('xxx'); // object

typeof null; // object

instanceof运算符返回一个布尔值,可以用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,通俗说就是用于判断某个实例是否属于某构造函数。再通俗一点就是,只要右边变量的prototype在左边变量的原型链上就返回 true,否则返回 false:

const result = [] instanceof Array; // true

const Person = function() {};
const p = new Person();
p instanceof Person; // true

const message = new String('xxx');
message instanceof String; // true

2.vue

说说vue的nextTick方法

nextTick 方法是在 Vue.js 中常见的一种异步更新 DOM 的机制。它的原理是利用 JavaScript 的事件循环机制以及浏览器的渲染流程来实现延迟执行 DOM 更新操作。它的出现主要是为了解决 Vue 的异步更新导致的 DOM 更新后的操作问题。

在 Vue 中,数据的变化会触发重新渲染 DOM,但实际上,Vue 的数据更新是异步的。也就是说,当我们修改了 Vue 实例的数据后,并不会立即进行 DOM 更新,而是在下一个事件循环中才会进行。

这个异步更新机制的设计是为了优化性能。Vue 会对进行多次数据变化进行合并,然后在下一个事件循环中进行一次性的 DOM 更新,从而减少不必要的 DOM 操作,提高性能。

然而,由于异步更新的机制,有时候可能在修改数据后需要立即执行一些 DOM 操作,例如获取到更新后的 DOM 元素、更新后的样式计算、触发一些特定事件等。这时候就需要使用 nextTick 方法了。

nextTick 方法是 Vue 提供的一个实用工具,它能够将回调函数延迟到下一个 DOM 更新循环之后执行。也就是说,通过 nextTick 方法,我们可以确保在 DOM 更新完成后执行某些操作。

谈谈v-if,v-show的区别

vuev-showv-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示。

控制手段:v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除

编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换

编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染。

说说vue的生命周期

  1. beforeCreate(创建前):
    • 在组件实例被创建之前立即调用。
    • 此时组件的数据和事件还未初始化。
  2. created(创建后):
    • 在组件实例被创建后立即调用。
    • 组件的数据已经初始化,但此时还未挂载到 DOM。
  3. beforeMount(挂载前):
    • 在组件挂载到 DOM 之前立即调用。
    • 此时模板编译完成,但尚未将组件渲染到页面上。
  4. mounted(挂载后):
    • 在组件挂载到 DOM 后立即调用。
    • 此时组件已经渲染到页面上,可以进行 DOM 操作。
  5. beforeUpdate(更新前):
    • 在组件数据更新之前立即调用。
    • 在此钩子函数内,你可以访问之前的状态,但此时尚未应用最新的数据。
  6. updated(更新后):
    • 在组件数据更新后立即调用。
    • 此时组件已经重新渲染,可以进行 DOM 操作。
  7. beforeDestroy(销毁前):/beforeUnmount(Vue3) :
    • 在组件销毁之前立即调用。
    • 此时组件仍然可用,你可以执行一些清理工作。
  8. destroyed(销毁后):/unmounted(Vue3):
    • 在组件销毁后立即调用。
    • 此时组件已经被完全销毁,不再可用。

lifecycle(1).png

谈谈什么是vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理库。它主要用于管理 Vue.js 应用中的共享状态(如数据、状态、配置信息等),以便更好地组织、维护和跟踪应用中的数据流。Vuex 的核心思想是将应用中的状态集中存储在一个全局的 store 中,使得状态的变化可预测且可维护。

Vuex是集中于MVC模式中的Model层,规定所有的数据操作必须通过 action - mutation - state 的流程来进行,再结合Vue的数据视图v-moder等双向绑定特性来实现页面的展示更新。

  • State(状态): 存储应用程序的状态数据,通常是一个 JavaScript 对象。
  • Mutations(突变): 用于修改状态的方法。每个 mutation 都有一个类型(type)和一个处理函数,用来执行实际的状态修改操作。
  • Actions(动作): 类似于 mutations,但是它可以包含异步操作,通常用于处理与服务器交互、数据获取等。Actions 负责提交 mutations 来修改状态。
  • Getters(计算属性): 用于从状态中派生出一些新的数据,类似于计算属性,可以被组件直接使用。
  • Store(存储): 将状态、mutations、actions、getters 集中管理的对象,是 Vuex 的核心。

优点:

  1. 集中管理:Vuex 可以让我们将应用中所有的组件共享的状态和数据集中管理,从而减少了代码的冗余和重复。
  2. 易于扩展:Vuex 的状态管理模式和架构清晰,易于理解和扩展。同时还提供了一些钩子函数和插件机制来方便扩展和自定义应用的状态管理。
  3. 调试工具:Vuex 提供了一些调试工具和开发者工具来方便测试和调试应用程序的状态管理。
  4. 开发效率:通过使用 Vuex,我们可以更快地开发和维护应用程序,减少了代码的复杂性和维护成本。

缺点:

  1. 状态冗余:如果 Vuex 中存储的状态数据过于冗余,会导致存储和读取数据的性能低下。因此,在设计时需要仔细考虑存储哪些状态数据。
  2. 多层嵌套:如果 Vuex 中的状态数据被多层嵌套,那么读取和更新数据的性能会受到影响。因此,应该避免过多的数据层级嵌套。
  3. 不必要的状态更新:如果通过 mutations 无需更新组件,或者组件不关注该状态的变化,仍然强制执行更新操作,就会出现不必要的重新渲染,影响应用性能。
  4. 大数据量的状态更新:如果 Vuex 中存储的状态数据比较大,并且频繁发生变化,可能会导致过多的数据更新操作,影响应用的性能。
  5. 大量组件订阅:如果大量组件订阅 Vuex 中的状态数据,可能会导致性能问题。这时,考虑使用计算属性或父子组件传递数据来替代 Vuex。

谈谈组件通信有哪些

1. Props(属性):

  • 父组件可以通过 props 向子组件传递数据。子组件通过 props 接收数据并在自己的模板中使用。
  • 这是一种单向数据流的方式,父组件向子组件传递数据。

2. 自定义事件:

  • 子组件可以通过触发自定义事件来向父组件通知事件发生。父组件可以监听这些事件并执行相应的操作。
  • 这是一种从子组件到父组件的通信方式。
  • 通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级

3. 状态管理(如Vuex):

  • 对于大型应用程序,可以使用状态管理库如 Vuex 来管理应用的状态。它提供了一个集中的状态存储,所有组件都可以访问和修改其中的数据。
  • 这是一种跨组件通信的高级方式。

4. 依赖注入provide/inject:

  • Vue.js 提供了依赖注入机制,允许你在祖先组件中注册一些数据,然后在后代组件中访问这些数据,而不需要通过 props 一层层传递。
  • 依赖注入通常用于一些全局配置或主题样式的传递。

谈谈computed,watch

computed:

支持缓存,根据依赖数据动态计算显示计算结果,且计算结果被缓存。一个计算属性只会在其响应式依赖更新时才重新计算,这意味着变量不改变,无论访问多少次都不用重新执行getter,而立即返回先前的计算结果。

我们应该注意的是:

1.计算属性的getter应该只做计算而没有其他副作用(不要在 getter 中做异步请求或者更改 DOM

2.计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。

watch:

watch它是一个对data的数据监听回调, 当依赖的data的数据变化时, 会执行回调。在回调中会传入newVal和oldVal两个参数。 Vue实列将会在实例化时调用$watch(), 他会遍历watch对象的每一个属性。

watch有一个特点是: 第一次初始化页面的时候, 是不会去执行age这个属性监听的, 只有当age值发生改变的时候才会执行监听计算. 因此我们需要修改下我们的 watch的方法,需要引入handler方法和immediate属性, 代码如下所示:

watch: {
        age: {
          handler(newVal, oldVal) {
            this.basicMsg = '今年' + newVal + '岁' + ' ' + this.single;
          },
          immediate: true;
        }
      }

watch里面有一个属性为deep,含义是:是否深度监听某个对象的值, 该值默认为false。 受JS的限制, Vue不能检测到对象属性的添加或删除的。它只能监听到obj这个对象的变化,比如说对obj赋值操作会被监听到。 deep实现机制是: 监听器会一层层的往下遍历, 给对象的所有属性都加上这个监听器。当然性能开销会非常大的。

计算属性属于多对一的一种业务场景上的运用,多个值生成一个值,或者说这一个值的结果需要多个值的依赖,比如统计之类的。而watch监听的话,却是相反的,往往是某一个值的变动,需要影响到一个或者多个变量时需要用到。

说说require,import的区别

importimport() 都是 ES6 中用于导入模块的语句,而 require() 则是 Node.js 中用于导入模块的函数。

使用 import 语句导入模块时,模块会被静态加载,也就是在编译时就已经确定了导入的模块;import()require() 都是动态加载模块的方式。它们都允许在代码运行时根据需要加载模块,而不是在编译时就将所有模块都加载进来。不过两者的实现方式略有不同:import() 是基于 Promise 的异步加载,而 require() 是同步加载模块。

在整个应用程序中,使用 importimport() 语句导入的模块都是单例模式,也就是共用同一个模块实例,而使用 require() 导入的模块则会因为复制而产生多个实例。

importimport() 语句支持模块的默认导出和命名导出,而 require() 只支持模块的默认导出 (module.exports) 导出。

3.算法

说说垂直水平居中的方法有哪些

1.利用定位+margin:auto

<style>
    .father{
        width:500px;
        height:300px;
        border:1px solid #0a3b98;
        position: relative;
    }
    .son{
        width:100px;
        height:40px;
        background: #f0a238;
        position: absolute;
        top:0;
        left:0;
        right:0;
        bottom:0;
        margin:auto;
    }
</style>
<div class="father">
    <div class="son"></div>
</div>

2.利用定位+margin:负值/transform

<style>
    .father {
        position: relative;
        width: 200px;
        height: 200px;
        background: skyblue;
    }
    .son {
        position: absolute;
        top: 50%;
        left: 50%;
        margin-left:-50px;
        margin-top:-50px;
        /* transform: translate(-50%,-50%); */
        /* 这两原理都一样,将元素位移自己宽度和高度的-50% */
        width: 100px;
        height: 100px;
        background: red;
    }
</style>
<div class="father">
    <div class="son"></div>
</div>

3.table布局

<style>
    .father {
        display: table-cell;
        width: 200px;
        height: 200px;
        background: skyblue;
        vertical-align: middle;
        text-align: center;
    }
    .son {
        display: inline-block;
        width: 100px;
        height: 100px;
        background: red;
    }
</style>
<div class="father">
    <div class="son"></div>
</div>

4.flex布局

<style>
    .father {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 200px;
        height: 200px;
        background: skyblue;
    }
    .son {
        width: 100px;
        height: 100px;
        background: red;
    }
</style>
<div class="father">
    <div class="son"></div>
</div>

5.grid布局

<style>
    .father {
            display: grid;
            align-items:center;
            justify-content: center;
            width: 200px;
            height: 200px;
            background: skyblue;

        }
        .son {
            width: 10px;
            height: 10px;
            border: 1px solid red
        }
</style>
<div class="father">
    <div class="son"></div>
</div>

说说数组去重的方法

1.Set去重

const setData = Array.from(new Set(arr));

2.双重for循环

/双重循环去重
const handleRemoveRepeat = (arr) => {
    for (let i=0,len = arr.length; i < len; i++) {
        for (let j = i + 1; j < len; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1);
                j--;
                len--;
            }
        }
    }
    return arr;
};

3.indexOf去重

//去重
const handleRemoveRepeat = (arr) => {
    let repeatArr = [];
    for (let i = 0,len = arr.length ; i < len; i++) 
     if (repeatArr.indexOf(arr[i]) === -1)  repeatArr.push(arr[i])
    return repeatArr;
}

4.includes去重

const handleRemoveRepeat = (arr) => {
    let repeatArr = [];
    for (let i = 0,len = arr.length ; i < len; i++)
        if (!repeatArr.includes(arr[i])) repeatArr.push(arr[i])
    return repeatArr;
}

5.filter配合indexOf去重(确实趣味性十足,没想过)

//去重
const handleRemoveRepeat = (arr) => arr.filter((item,index) => arr.indexOf(item,0) === index);

判断是否是数组

1.Array.isArray()

是ES5新增的方法,用于确定传递的值是否是一个数组,如果是数组,则返回 true,否则返回 false。

let arr = [];
console.log(Array.isArray(arr)); // true
Array.isArray(Array.prototype); // true
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array()); // true
Array.isArray(new Array("a", "b", "c", "d")); // true

2.Object.prototype.toString

每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。

默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]" 字符串,其中 type 是对象的类型。

可以通过 toString() 来获取每个对象的类型。为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为 thisArg。用法如下:

var toString = Object.prototype.toString;

toString.call(new Date); // [object Date]
toString.call(new String); // [object String]
toString.call(Math); // [object Math]

//Since JavaScript 1.8.5
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]
// 判断数组----------------------------------------------------------------------------------------

let arr = [];
console.log(Object.prototype.toString.call(arr) === "[object Array]"); // true

3.constructor(此条及以后不需要记忆,不太好用)

Object 的每个实例都有构造函数 constructor,用于保存着用于创建当前对象的函数.需要注意的是,constructor 有被修改的风险,判断结果不一定准确。

let arr = [];
console.log(arr.constructor === Array); // true

let arr = [1, 2, 3];
arr.constructor = function () { }
console.log(arr.constructor === Array); // false

(。・∀・)ノ゙嗨,你猜怎么着,不写了

深拷贝和浅拷贝

先不写.......

深拷贝 就是完整的拷贝了一份一模一样结构的数据, 拷贝后的数据和源数据是没有任何关联的, 修改原数据不会修改到拷贝后的数据.

const res = JSON.parse(JSON.stringify(obj))
/*
NaN Infinity -Infinity 会被序列化为 null
Symbol undefined function 会被忽略(对应属性会丢失)
Date 将得到的是一个字符串
拷贝 RegExp Error 对象,得到的是空对象 {}
*/

浅拷贝 会新建一个对象, 拷贝对象的所有属性值, 对于 基本数据 来说就是拷贝一份对应的值, 但是对于 引用数据 则是拷贝一份 引用数据 的引用地址.

// 将所有的源对象的可枚举属性复制到目标对象中
// 将 base1 base2 中的属性添加到, 对象 {} 中
// base1 base2 存在相同属性, 会被 base2 的覆盖掉
const res = Object.assign({}, base1, base2)
// --------------------------------------------
// 展开运算符...

说说排序方法

JavaScript实现十大排序算法(图文详解) - 掘金 (juejin.cn)

1.冒泡排序

冒泡排序的特点,是一个个数进行处理。第i个数,需要与后续的len-i-1个数进行逐个比较。

function bubbleSort(arr){
	const len = arr.length;
	for(let i = 0; i < len - 1; i++){
		for(let j = 0; j < len - i - 1; j++){
			if(arr[j] > arr[j+1]){
				const tmp = arr[j+1];
				arr[j+1] = arr[j];
				arr[j] = tmp;
			}
		}
	}

	return arr;
}

2.快速排序

通过选定一个数字作为比较值,将要排序其他数字,分为 >比较值<比较值,两个部分。并不断重复这个步骤,直到只剩要排序的数字只有本身,则排序完成。

function quickSort(arr){

	sort(arr, 0, arr.length - 1);
	return arr;


	function sort(arr, low, high){
		if(low >= high){
			return;
		}
	
		let i = low;
		let j = high;
		const x = arr[i]; // 取出比较值x,当前位置i空出,等待填入
		while(i < j){
			// 从数组尾部,找出比x小的数字
			while(arr[j] >= x && i < j){
				j--;
			}
			// 将空出的位置,填入当前值, 下标j位置空出
			// ps:比较值已经缓存在变量x中
			if(i < j){
				arr[i] = arr[j]
				i++;
			}

			// 从数组头部,找出比x大的数字
			while(arr[i] <= x && i < j){
				i++;
			}
			// 将数字填入下标j中,下标i位置突出
			if(i < j){
				arr[j] = arr[i]
				j--;
			}
			// 一直循环到左右指针i、j相遇,
			// 相遇时,i==j, 所以下标i位置是空出的
		}

		arr[i] = x; // 将空出的位置,填入缓存的数字x,一轮排序完成

		// 分别对剩下的两个区间进行递归排序
		sort(arr, low, i - 1);
		sort(arr, i+1, high);
	}
}

4.浏览器

浏览器输入url之后发生了什么

当我们向浏览器的地址栏输入URL的时候,网络会进行一系列的操作,最终获取到我们所需要的文件,然后交给浏览器进行渲染。

  • URL解析:浏览器先会判断输入的字符是不是一个合法的URL结构,如果不是,浏览器会使用搜索引擎对这个字符串进行搜索。
  • DNS 解析:缓存判断 + 查询IP地址。判断是正确的URL格式之后,DNS会在我们的缓存中查询是否有当前域名的IP地址。如果没有,浏览器会去根域名服务器中查找,如果还没有就去顶级域名服务器中查找,最后是权威域名服务器。找到IP地址后,将它记录在缓存中,供下次使用。
  • TCP 连接:TCP 三次握手。
  • SSL/TLS四次握手(只有https才有这一步)
  • 浏览器发送请求
  • 服务器响应请求并返回数据
  • 浏览器解析渲染页面
  • 断开连接:TCP 四次挥手

说说浏览器怎么渲染页面

当网络线程接收到一个 HTML 文件后,会产生一个渲染任务,放到消息队列中。

渲染主线程会在事件循环机制下,开启渲染流程。

整个渲染流程分为 八大阶段,分别是:

  • parse - HTML 解析
  • style - 样式计算
  • layout - 布局
  • compositor - 分层
  • paint - 绘制
  • tiling - 分块
  • raster - 光栅化
  • draw - 画

这八大步骤中,涉及到了 渲染主线程合成线程光栅化线程GPU进程 等多个线程和进程分工合作,每个阶段都有明确的输入和输出,上一个阶段的输出会成为下一个阶段的输入。

你知道的前端优化有哪些

压缩和合并文件:压缩HTML、CSS、JavaScript,然后合并它们以减少HTTP请求。

延迟加载:只在需要时加载内容。

懒加载:在用户滚动页面时才加载图像和其他媒体。

使用缓存:使用浏览器缓存来减少资源加载时间。

压缩图像:使用压缩后的图片格式,如JPEG,以减少文件大小,从而加快加载速度。

说说垃圾回收机制

引用计数(Reference Counting),这其实是早先的一种垃圾回收算法,它把 对象是否不再需要 简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

标记清除(Mark-Sweep),目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎 都在采用

标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不同浏览器的 JavaScript引擎 在运行垃圾回收的频率上有所差异。

它从一组根对象开始,标记所有可以访问到的对象。然后,它清除未被标记的对象,因为它们不再可达。