javascript面试题

1 闭包

定义

  • 它指的是一个函数能够记住并访问其创建时的词法作用域,即使该函数在其原始作用域之外被执行。

闭包的创建

  • 创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量

优缺点

  • 优点
    • 1.可以让变量长期储存在内存中
    • 2.封装对象的私有属性和私有方法,可以避免全局变量污染
  • 缺点
    • 1.占内存
    • 2.使用不当容易内存泄露

常用用途

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

闭包实际场景运用的例子

  • 1.比如常见的防抖节流
// 防抖
function debounce(fn, delay = 300) {
  let timer; //闭包引用的外界变量
  return function () {
    const args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}
  • 2.事件处理器中的闭包
document.addEventListener('click', function() {
  console.log('The document was clicked!');
});
  • 3.异步编程中的回调
function fetchData(url, callback) {
  fetch(url)
    .then((response) => response.json())
    .then((data) => callback(data))
    .catch((error) => console.error("Error:", error));
}
fetchData('https://api.example.com/data', (data) => {
  console.log(data); // 使用闭包访问外部变量
});
  • 4.柯里化(Currying)
function add(x) {
  return function(y) {
    return x + y;
  };
}

const addFive = add(5);
console.log(addFive(3)); // 8
  • 5.数据封装和对象属性的私有化
function createObject() {
  let privateProperty = "I'm a secret";

  return {
    getPropertyValue: function() {
      return privateProperty;
    },
    setProperty: function(value) {
      privateProperty = value;
    }
  };
}

const myObj = createObject();
console.log(myObj.getPropertyValue()); // 访问私有属性
myObj.setProperty("New secret");

高频考察点

如何解决循环输出问题

for(var i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}

上面这段代码执行之后,从控制台执行的结果可以看出来,结果输出的是 5 个 6,那么一般面试官都会先问为什么都是 6?我想让你实现输出 1、2、3、4、5 的话怎么办呢?

因此结合本讲所学的知识我们来思考一下,应该怎么给面试官一个满意的解释。你可以围绕这两点来回答。

  • setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行
  • 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

1.利用 IIFE

for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}

2.使用 ES6 中的 let

for(let i = 1; i <= 5; i++){
  setTimeout(function() {
    console.log(i);
  },0)
}

3.定时器传入第三个参数

for(var i=1;i<=5;i++){
  setTimeout(function(j) {
    console.log(j)
  }, 0, i)
}

2 说说你对作用域和作用域链的理解

作用域

作用域是定义变量的区域,它有一套访问变量的规则,浏览器引擎根据这套规则在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找

类型

  • 全局作用域
  • 函数作用域
  • 块级作用域,ES6 中的 let、const 就可以产生该作用域
    • ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。

作用域链

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。

本质

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

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

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

原型

JavaScript的所有对象中都包含了一个 [__proto__] 内部属性,这个属性所对应的就是该对象的原型。JavaScript的函数对象(非箭头函数),除了原型 [__proto__] 之外,还预置了 prototype 属性,当函数对象作为构造函数创建实例时,该 prototype 属性值将被作为实例对象的原型 [__proto__]

原型对象默认有一个属性constructor ,值为对应的构造函数;另外,有一个属性__proto__,值为Object.prototype

相关属性

  • prototype(显示原型)
    • 在JavaScript中,每个构造函数都有一个prototype属性,它指向一个对象。当你使用new关键字创建一个对象(即构造函数)时,这个新对象的内部将有一个__ proto__属性指向其构造函数的prototype对象,可以通过Object.getPrototypeOf()方法访问。同时这个对象可以继承得到原型上的所有属性和方法。
    • 箭头函数没有prototype属性。
  • __ proto__(隐式原型)
    • 所有对象都有__proto__属性, 当用构造函数实例化(new)一个对象时,会将新对象的__proto__属性指向 构造函数的prototype。
    • 函数的__proto__都是指向Function的prototype(说明所有函数都属通过new Function创建出来的)
    • 构造函数new出来的对象__proto__指向构造函数的prototype
    • 非构造函数实例化出的对象的__proto__指向Object的prototype(非构造函数创建出来的对象一般是let a={…},相当于是通过new Object创建出来的)
    • Object的prototype的__proto为null
  • constructor
    • 所有的原型对象都会自动获得一个 constructor(构造函数)属性,这个属性(是一个指针)指向 prototype 属性所在的函数(Person)

prototype、proto、constructor之间的关系

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

原型链

原型链是一系列原型对象的链接,它允许属性和方法的继承。当试图访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript解释器会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的末端(通常是Object.prototype,其末端是null)。

作用
通过原型链,对象可以继承另一个对象的属性和方法。这是JavaScript实现继承的主要方式。

相关函数

  • hasOwnProperty

    • 可以用来检查对象自身是否含有某个属性,返回值是布尔值,当属性不存在时不会向上查找对象原型链,hasOwnProperty是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。
    var obj = { a: 1 };
    obj.b = 2;
    console.log(obj.hasOwnProperty('a')); // true
    console.log(obj.hasOwnProperty('b')); // true
    console.log(obj.hasOwnProperty('toString')); // false, 继承自Object.prototype
    
  • getOwnPropertyNames

    • 可以获取对象所有的自身属性,返回值是由对象自身属性名称组成的数组,同样不会向上查找对象原型链。
    var obj = { a: 1, b: 2 };
    Object.defineProperty(obj, 'c', {
      enumerable: false,
      value: 3
    });
    console.log(Object.getOwnPropertyNames(obj)); // ['a', 'b', 'c']
    
  • isPrototypeOf

    • 一个方法,用于检查一个对象是否是另一个对象的原型。
    var proto = { a: 1 };
    var obj = Object.create(proto);
    console.log(proto.isPrototypeOf(obj)); // true
    
  • getPrototypeOf

    • 返回对象 obj 的原型。
    const obj = {};
    console.log(Object.getPrototypeOf(obj)); // 输出:{}
    

为什么引入原型和原型链?
JavaScript的原型是为了实现对象间的联系,解决构造函数无法数据共享而引入的一个属性,而原型链是一个实现对象间联系即继承的主要方法。

原型对象、实例对象、构造函数之间的关系
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

原型链判断

请写出下面的答案

Object.prototype.__proto__;
Function.prototype.__proto__;
Object.__proto__;
Object instanceof Function;
Function instanceof Object;
Function.prototype === Function.__proto__;  

答案

Object.prototype.__proto__; //null
Function.prototype.__proto__; //Object.prototype
Object.__proto__; //Function.prototype
Object instanceof Function; //true
Function instanceof Object; //true
Function.prototype === Function.__proto__; //true

4 请解释什么是事件代理

事件代理(Event Delegation),又称之为事件委托。一种利用事件冒泡的特性,在父元素上统一处理子元素的事件的技术

优缺点

  • 优点
    • 减少内存消耗,提高性能:减少了事件处理函数的数量,避免了给每个子元素都绑定事件处理函数的开销。
    • 动态内容处理:对于动态添加的子元素,无需重新绑定事件处理函数,事件代理仍然能够正常工作。
    • 简化代码
  • 缺点
    • 事件类型限制:事件代理只适用于冒泡的事件,不适用于不冒泡的事件(如focus和blur)。
    • 事件目标识别:需要在事件处理函数中检查event.target来确定哪个子元素触发了事件,这可能会增加一些逻辑处理。
    • 事件委托链中断:如果在事件冒泡路径上的某个元素上阻止了事件冒泡(通过调用event.stopPropagation()),则事件代理将不会工作。
<ul id="myList">
 <li>Item 1</li>
 <li>Item 2</li>
 <li>Item 3</li>
</ul>

// 使用事件代理绑定点击事件

var myList = document.getElementById('myList');
myList.addEventListener('click', function(event) {
 if (event.target.tagName === 'LI') {
  console.log('Clicked on:', event.target.textContent);
 }
});

5 Javascript如何实现继承?

1. 原型链继承

在原型链继承中,通过将子构造函数的原型对象指向父构造函数的实例,实现了继承。

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

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

var child = new Child();
child.sayHello(); // Hello

优点:子对象可以访问父对象原型链上的属性和方法。
缺点:所有子对象共享同一个原型对象,对原型对象的修改会影响到所有子对象。

2. 构造函数继承

构造函数继承是通过在子构造函数中调用父构造函数来实现继承。在构造函数继承中,通过在子构造函数中使用call()或apply()方法,将父构造函数的上下文设置为子对象的上下文,从而实现继承。

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

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

var child = new Child('Child');
console.log(child.name); // Child

缺点:构造函数继承只能继承父构造函数的实例属性,无法继承父构造函数的原型对象上的方法。

3. 组合继承(原型链继承 + 构造函数继承)

组合继承结合了原型链继承和构造函数继承,既继承了父构造函数的属性,又继承了父构造函数原型对象上的方法。在组合继承中,通过调用父构造函数的方式实现属性的继承,通过将子构造函数的原型对象指向父构造函数的实例实现方法的继承。

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

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

var child = new Child('Child');
child.sayHello(); // Hello

优点:既能够继承父构造函数的属性,又能够继承父构造函数原型对象上的方法。
缺点:缺点是在创建子对象时会调用两次父构造函数,一次是在设置原型时,一次是在创建子对象时。这样会产生一些不必要的开销。

4. 原型式继承

原型式继承是通过Object.create()方法来实现继承。

var parent = {
  name: 'Parent',
  sayHello: function() {
    console.log('Hello');
  }
};

var child = Object.create(parent);
console.log(child.name); // Parent
child.sayHello(); // Hello

原型式继承的本质是创建一个新对象,将其原型对象指向另一个已有的对象。
缺点:这种方式可以实现属性和方法的继承,但是不能传递构造函数的参数。

5. 寄生式继承

寄生式继承是在原型式继承的基础上进行了一些额外的操作,通常是在新对象上增加一些额外的属性或方法,然后返回这个新对象。这种方式类似于工厂模式,通过一个函数来创建对象并增强其功能。

function createAnother(original) {
  // 通过调用 Object.create() 创建一个新对象,该对象的原型指向 original
  var clone = Object.create(original);

  // 增强这个新对象
  clone.sayHi = function() {
    console.log('Hi from the cloned object');
  };

  // 返回增强后的对象
  return clone;
}

var originalObject = {
  name: 'Original',
  sayHello: function() {
    console.log('Hello from the original object');
  }
};

// 使用寄生式继承创建一个新对象
var clonedObject = createAnother(originalObject);

clonedObject.sayHi(); // 输出 "Hi from the cloned object"

优点:写法简单,不需要单独创建构造函数。
缺点:通过寄生式继承给对象添加函数会导致函数难以重用。使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

6.寄生组合继承(原型式继承+寄生式继承)

组合继承是常用的经典继承模式,不过,组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数;一次是在创建子类型的时候,一次是在子类型的构造函数内部。寄生组合继承就是为了降低父类构造函数的开销而实现的。

通过借用构造函数来继承父类属性,通过原型链的混成形式来继承原型对象上的方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建父类原型的副本
  prototype.constructor = subType; // 修改副本的 constructor 指向子类
  subType.prototype = prototype; // 将子类的原型指向修改后的副本
}

// 定义父类 SuperType
function SuperType(name) {
  this.name = name;
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

// 定义子类 SubType
function SubType(name, age) {
  // 继承父类的实例属性
  SuperType.call(this, name);
  this.age = age;
}

// 继承父类的方法和共享属性
inheritPrototype(SubType, SuperType);

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

var instance = new SubType('Alice', 25);
instance.sayName(); // 输出 "Alice"
instance.sayAge(); // 输出 25

优点: 高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;

缺点: 代码复杂

7.ES6类继承

在ES6中引入了类的概念,通过class关键字和extends关键字可以实现类的继承。

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

  sayHello() {
    console.log('Hello');
  }
}

class Child extends Parent {
  constructor(name) {
    super(name);
  }
}

const child = new Child('Child');
console.log(child.name); // Child
child.sayHello(); // Hello

参考链接: JS中的八种继承方法

6 谈谈This对象的理解

javaScript 中的 this 对象表示当前执行代码的上下文。this的值取决于函数是如何被调用的,而不是函数在哪里被声明的。

以下是几种常见的this值的确定方式:

1. 普通函数的调用,this指向的是Window

var name = '卡卡';
function cat(){
  var name = '有鱼';
  console.log(this.name);//卡卡
  console.log(this);//Window {frames: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
}
cat();

2. 对象的方法,this指的是该对象

一层作用域链时,this指的该对象

var name = '卡卡';
var cat = {
  name:'有鱼',
  eat:function(){
    console.log(this.name);//有鱼
  }
}
cat.eat();

多层作用域链时,this指的是距离方法最近的一层对象

var name = '卡卡';
var cat = {
  name:'有鱼',
  eat1:{
    name:'年年',
    eat2:function(){
      console.log(this.name);//年年
    }
  }
}
cat.eat1.eat2();

如果cat.eat1.eat2这个结果赋值给一个变量eat3,则eat3()的值是卡卡,这个是因为经过赋值操作时,并未发起函数调用,eat3()这个才是真正的调用,而发起这个调用的是根对象window,所以this指的就是window,this.name=卡卡

var eat3 = cat.eat1.eat2;
eat3(); // 卡卡

3. 构造函数的调用,this指的是实例化的新对象

var name = '卡卡';
function Cat(){
  this.name = '有鱼';
  this.type = '英短蓝猫';
}
var cat1 = new Cat();
console.log(cat1);// 实例化新对象 Cat {name: "有鱼", type: "英短蓝猫"}
console.log(cat1.name);// 有鱼

4. apply和call调用时,this指向参数中的对象

var name = '有鱼';
function eat(){
  console.log(this.name);
}
var cat = {
  name:'年年',
}
var dog = {
  name:'高飞',
}

eat.call(cat);// 年年
eat.call(dog);// 高飞

5. 匿名函数调用,指向的是全局对象

var name = '卡卡';
var cat = {
  name:'有鱼',
  eat:(function(){
    console.log(this.name);//卡卡
  })()
}
cat.eat;

6. 定时器中调用,指向的是全局变量

var name = '卡卡';
var cat = setInterval(function(){
  var name = '有鱼';
  console.log(this.name);// 卡卡
  clearInterval(cat);
},500);

7. 箭头函数调用

箭头函数没有自己的this上下文,它会捕获其所在上下文的this值,即使用箭头函数时,this的值与其被声明时所在的作用域中的this值相同。

var name = 'window';
var student = {
    name: '若川',
    doSth: function(){
        // var self = this;
        var arrowDoSth = () => {
            // console.log(self.name);
            console.log(this.name);
        }
        arrowDoSth();
    },
    arrowDoSth2: () => {
        console.log(this.name);
    }
}
student.doSth(); // '若川'
student.arrowDoSth2(); // 'window'

let stuTmp = student.doSth;
stuTmp();//window

7 谈谈你对(事件循环)Event Loop的理解

事件循环(Event Loop)是 JavaScript 中一种运行机制,它负责管理和调度 JavaScript 代码的执行顺序,保证异步代码的正确执行。在 JavaScript 中,事件循环是基于单线程的执行模型。这意味着所有的 JavaScript 代码都是在同一个线程上执行的,因此必须通过事件循环来处理异步代码,以避免阻塞整个应用程序。

Event Loop 的核心组成部分

1. 执行栈(Call Stack)

  • JavaScript 代码执行时,所有同步任务都在执行栈中执行。
  • 当一个函数执行时,它会被添加到栈顶,执行完成后从栈顶移除。

2. 事件队列(Event Queue)

  • 异步任务完成后,回调函数会被放入事件队列中等待执行。
  • 事件队列遵循先进先出(FIFO)的原则。

3. Event Loop

  • 负责循环检查执行栈和任务队列的状态,并决定下一步执行的代码。
  • Event Loop 检查执行栈是否为空,如果执行栈为空,Event Loop 会从事件队列中取出第一个任务,放入执行栈中执行。

4. 宏任务队列(Macro Task Queue)/微任务队列(Micro Task Queue)

  • 宏任务队列包括如script整体代码、setTimeout、setInterval、setImmediate、I/O、UI 渲染、用户交互事件(比如鼠标点击、滚动页面、放大缩小等)、postMessage、 MessageChannel等。
  • 微任务队列包括 Promise 回调、MutationObserver、async/await、process.nextTick、Object.observe等。
  • 当执行栈清空后,Event Loop 首先处理所有微任务队列中的任务,然后才处理下一个宏任务。

由于JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,会在渲染前,将执行期间所产生的所有微任务都执行完,然后GUI渲染线程开始工作,对页面进行渲染

宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...

Event Loop 的工作流程

  • 首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行
  • 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
  • 当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行
  • 任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行
  • 当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。

async和await
async在函数定义时使用,用async定义的函数默认返回一个Promise实例,可以直接.then(async还可以定义对象的方法),如果async定义的函数执行返回的不是一个promise对象,那么就会给返回值包装成一个promise对象(将返回值放进promise实例的resolve方法中当做参数)。

js会把await左边和下面的代码当做一个微任务整体放进了等待任务队列中。

用法

    1. 如果await右侧是同步的代码,就会让同步代码执行;如果执行的是一个函数,还会把函数的返回值给到await左边的变量
let p;
async function f3() {
    p = await 18;
    console.log(p);
}

f3();
console.log(1);
console.log(p);
//  结果是1,undefined,18

js执行的时候是从右向左执行的,先执行18然后遇到await会将await左边的连同下面的都放进微任务中去,所以外面的p打印会是undefined,然后同步代码执行完再执行微任务的时候将其执行,给p赋值,打印p。

  • 如果await右侧是一个Promise实例,或者一个方法返回了Promise实例,await会等着Promise的实例resolve,并且在实例resolve之前,await后面的代码不执行;并且还会拿到Promise在resolve时传入的值,并且赋值给等号左侧变量;
async function f(){
    return 10;
}

async function f3() {
    let p = await f();
    console.log(p);
}

f3();
console.log(1);
// 打印1,10,上面说了async定义的函数后面如果返回的不是一个promise对象而是一个普通值就会默认包装成一个promise对象,
//并将其返回值赋值给await左边的变量,如果我们不使用async而是写一个promise对象,如下:

function f(){
    return new Promise(((resolve, reject) => {
        resolve(10);
    }))
}

async function f3() {
    let p = await f();
    console.log(p);
}

f3();
console.log(1);
setTimeout(function () {
  console.log("1");
}, 0);
async function async1() {
  console.log("2");
  const data = await async2();
  console.log("3");
  return data;
}
async function async2() {
  return new Promise((resolve) => {
    console.log("4");
    resolve("async2的结果");
  }).then((data) => {
    console.log("5");
    return data;
  });
}
async1().then((data) => {
  console.log("6");
  console.log(data);
});
new Promise(function (resolve) {
  console.log("7");
  //   resolve() 
}).then(function () {
  console.log("8");//因为resolve被注释了,所以then的回调函数没有执行
});

//输出结果:247536 async2 的结果 1**加粗样式**  
async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('setTimeout')
}, 0)
async1();
new Promise(function (resolve) {
    console.log('promise1')
    resolve();
}).then(function () {
    console.log('promise2')
})
console.log('script end')
//输出结果为
//script start
//async1 start
//async2
//promise1
//script end
//async1 end
//promise2
//setTimeout

JS单线程又是如何实现异步的呢?

既然JS是单线程的,只能在一条线程上执行,又是如何实现的异步呢? 是通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制

浏览器的多线程

GUI 渲染线程 (负责渲染浏览器界面HTML元素)
JavaScript引擎线程(主要负责处理Javascript脚本程序,例如V8引擎,GUI渲染线程和JavaScript引擎线程互斥!)
事件触发线程(如鼠标点击、AJAX异步请求等)
定时触发器线程( setTimeout, setInterval )
异步http请求线程
事件轮询处理线程 ( 作用:轮询消息队列,event loop )

8 new操作符具体干了什么呢?

  1. 创建一个全新的空对象new 操作符首先创建一个空对象 {}
  2. 设置原型:这个新对象的原型(即内部的 __proto__ 属性)被设置为构造函数的 prototype 属性所指向的对象。这意味着新对象继承了构造函数 prototype 上定义的所有属性和方法。
  3. 设置this:将构造函数内部的 this 指向这个空对象。
  4. 执行构造函数:构造函数内部的代码被执行。在这个过程中,this 关键字指向新创建的对象,允许构造函数为新对象添加属性和方法。
  5. 返回新对象:如果构造函数没有返回一个对象类型的值(即返回 undefined 或者没有返回值),那么 new 操作符将返回步骤1中创建的新对象。如果构造函数返回了一个对象类型的值,那么这个返回的对象将作为 new 操作的结果,而不是步骤1中创建的对象。

实现简单的new方法

function myNew(constructor, ...args) {
  // 创建一个新的空对象
  const newObj = {};

  // 将新对象的原型链接到构造函数的原型对象
  Object.setPrototypeOf(newObj, constructor.prototype);

  // 将构造函数的作用域赋给新对象,并执行构造函数
  const result = constructor.apply(newObj, args);

  // 如果构造函数有显式返回一个对象,则返回该对象;否则返回新对象
  return result instanceof Object ? result : obj;
}

使用myNew方法

function Person(name, age) {
  this.name = name;
  this.age = age;
}

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

var poetry = myNew(Person, 'poetry', 25);
console.log(poetry.name); // 输出: poetry
console.log(poetry.age); // 输出: 25
poetry.sayHello(); // 输出: Hello, my name is poetry

9 Ajax原理

它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

手写简易的Ajax

// 手写简易ajax
/** 1. 创建连接 **/
var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本
/** 2. 连接服务器 **/
xhr.open('get', url, true)
/** 3. 发送请求 **/
xhr.send(null);
/** 4. 接受请求 **/
xhr.onreadystatechange = function(){
	if(xhr.readyState == 4){
		if(xhr.status == 200){
			success(xhr.responseText);
		} else { 
			/** false **/
			fail && fail(xhr.status);
		}
	}
}

promise封装

// promise 封装实现:

function getJSON(url) {
  // 创建一个 promise 对象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();

    // 新建一个 http 请求
    xhr.open("GET", url, true);

    // 设置状态的监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;

      // 当请求成功或失败时,改变 promise 的状态
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };

    // 设置错误监听函数
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };

    // 设置响应的数据类型
    xhr.responseType = "json";

    // 设置请求头信息
    xhr.setRequestHeader("Accept", "application/json");

    // 发送 http 请求
    xhr.send(null);
  });

  return promise;
}

readyState 属性的值如下:

0:请求未初始化(尚未调用 open())。

1:服务器连接已建立(已调用 open(),但尚未调用 send())。

2:请求已接收(已调用 send(),响应头和响应状态已可获取)。

3:请求处理中(正在下载响应体,响应体尚未完全接收)。

4:请求已完成,且响应已就绪(整个请求过程已结束,可以访问服务器的响应了)。

ajax 有哪些优缺点?

优点

1.提升用户体验:页面局部刷新,无需重新加载整个页面,使用户感受到更快的响应速度。
2.减轻服务器负担:只向服务器请求必要的数据,减少了不必要的数据传输,减轻了服务器的负载。
3.分离数据处理:允许前端和后端的逻辑分离,前端只关注用户界面和用户体验,后端只关注数据处理。
4.提高页面性能:通过异步请求,可以避免页面的全页刷新,从而提高页面性能。
5.易于维护:前端和后端的分离使得代码更容易维护和更新。

缺点
1.浏览器兼容性:老版本的浏览器可能不支持或不完全支持Ajax。
2.安全性问题:需要考虑跨站请求伪造(CSRF)和跨站脚本(XSS)等安全问题。
3.搜索引擎优化(SEO)问题:由于内容是动态加载的,可能会对搜索引擎优化(SEO)造成影响。

10 如何解决跨域问题

1. 通过jsonp跨域

JSONP是一种非官方的协议,它利用script标签可以跨域请求资源的特性来实现跨域访问。通过在请求的URL中添加一个callback参数,服务器响应时会将这个参数作为一个函数调用,从而将数据作为参数传递给这个函数。
这种方法只支持GET请求,且安全性较低,因为它容易受到XSS攻击。

<script src="http://example.com/api?callback=myCallback"></script>
<script>
  function myCallback(data) {
    console.log(data);
  }
</script>

2.CORS(Cross-Origin Resource Sharing)

CORS 是一种官方标准的解决跨域请求的方法,通过在服务器端设置响应头来控制是否允许跨域请求。在服务器端设置 Access-Control-Allow-Origin 等相关头部信息,来指定允许的来源域名。

const express = require('express');
const app = express();

// 允许所有域名访问
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  next();
});

// 路由和处理逻辑
// ...

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

3.代理服务器

通过在同源的服务器上设置一个代理服务,将请求转发到目标服务器。代理服务器接收到响应后再转发回前端应用,这样就绕过了浏览器的同源策略。

const express = require('express');
const axios = require('axios');
const app = express();

app.get('/api/data', (req, res) => {
  // 向目标服务器发送请求
  axios.get('http://api.example.com/data')
    .then((response) => {
      // 将目标服务器的响应返回给前端
      res.json(response.data);
    })
    .catch((error) => {
      res.status(500).json({ error: 'An error occurred' });
    });
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

4.PostMessage

PostMessage 是 HTML5 中定义的一种跨窗口通信机制,它允许来自不同源的窗口进行安全的通信。通过 PostMessage,一个窗口可以向另一个窗口发送消息,无论这两个窗口是否同源,从而实现跨域通信。

  • 发送消息
    • targetWindow 是目标窗口的引用,可以是 iframe 的 contentWindow。
    • message 是要发送的消息,可以是字符串或对象等。
    • targetOrigin 是目标窗口的源,用于限制消息发送的目标。通常应该使用具体的源,如 ‘https://example.com’,或者 ‘*’ 表示接受来自任意源的消息。
const targetWindow = document.getElementById('targetFrame').contentWindow;
const message = 'Hello from another domain!';
const targetOrigin = 'https://example.com';

targetWindow.postMessage(message, targetOrigin);
  • 接受消息
window.addEventListener('message', function(event) {
  // 确保消息来自可信任的源
  if (event.origin !== 'https://sender-domain.com') return;

  // event.data 包含发送来的数据
  console.log('Received message:', event.data);
});

5.Nginx 反向代理

通过 Nginx 配置反向代理,将跨域请求转发到同源接口,从而避免浏览器的同源策略限制。

6.WebSocket

使用 WebSocket 进行双向通信,WebSocket 不受同源策略的限制,可以实现跨域通信。

const socket = new WebSocket('ws://example.com/socket');

socket.onopen = () => {
  console.log('WebSocket connection established.');
  // 发送数据
  socket.send('Hello, server!');
};

socket.onmessage = (event) => {
  console.log('Received message from server:', event.data);
};

socket.onclose = () => {
  console.log('WebSocket connection closed.');
};

7.通过webpack devserver代理

如何配置 webpack-dev-server 的代理:
① 安装 Webpack 和 webpack-dev-server:

npm install --save-dev webpack webpack-dev-server

② 配置 webpack.config.js:
在 webpack.config.js 文件中,你可以配置 devServer 对象,并设置 proxy 属性。

module.exports = {
  // 其他配置项...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://api.example.com', // 设置代理目标地址
        pathRewrite: { '^/api': '' }, // 重写请求路径,去掉 '/api' 前缀
        changeOrigin: true, // 修改请求头中的 Origin 为目标地址
      },
    },
  },
};

我们配置了 devServer 的代理功能,当 /api 开头的请求发起时,会被代理到http://api.example.com。

注意,这里的配置是针对开发环境下的代理,当你构建生产环境的代码时,代理配置不会生效。

11 模块化

js 中现在比较成熟的有四种模块加载方案:

  • 第一种是 CommonJS 方案,是 Node.js 中使用的一种模块化规范,它通过 require 方法来加载模块,并通过 module.exports 对象来导出模块。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
  • 第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范
  • 第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
  • 第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块

CommonJS 和 ES6 中的模块化的两者区别?

  • CommonJS同步导入,ES6异步导入
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。
  • 但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • 后者会编译成 require/exports 来执行的

AMD 和 CMD 规范的区别?

  • 第一个方面是在模块定义时对依赖的处理不同。AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
  • 第二个方面是对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,不过它们的区别在于模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。

模块化开发的好处
1. 封装性:模块化开发通过封装相关功能和数据,将其隐藏在模块内部,只暴露必要的接口给外部使用。这种封装性可以帮助避免全局作用域污染、减少命名冲突,并且提高代码的安全性和可维护性。
2. 可重用性:模块化开发使得代码更易于重用。一个良好设计的模块可以在不同的项目中被多次使用,从而减少重复编写代码的工作量,提高开发效率。
3. 解耦性:模块化开发通过明确定义模块之间的接口和依赖关系,达到了解耦的效果。这意味着更改一个模块的实现不会影响其他模块,降低了系统的耦合度,使得代码更易于维护和扩展。
4. 可扩展性:随着系统需求的变化,模块化设计使得新功能的添加变得更加容易。

12 哪些操作会造成内存泄漏?

1. 意外的全局变量:意外创建的全局变量会一直存在于内存中,直到页面关闭。
2. 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
3. 事件监听器:如果为元素添加了事件监听器,但在元素被销毁前没有移除这些监听器,这些监听器会持续占用内存。
4. 定时器:未被正确关闭,导致所引用的外部变量无法被释放
5. 未释放的DOM元素引用
6. 循环引用:当两个对象相互引用且无法从根对象访问时,它们可能会因为循环引用而无法被垃圾回收。
7. 缓存:使用诸如localStorage或sessionStorage的缓存时,如果缓存数据未被清理,可能会造成内存泄漏。
8. 控制台日志(console.log)

13 常见web安全及防护方法

1. SQL注入:就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令

防护方法:

  • 防护措施包括对用户输入进行校验,使用参数化SQL,限制数据库权限等。

2. 跨站脚本攻击 (XSS):Xss(cross-site scripting)攻击指的是攻击者往Web页面里插入恶意html标签或者javascript代码。比如:攻击者在论坛中放一个看似安全的链接,骗取用户点击后,窃取cookie中的用户私密信息;或者攻击者在论坛中加一个恶意表单,当用户提交表单的时候,却把信息传送到攻击者的服务器中,而不是用户原本以为的信任站点

防护方法:

  • 对用户输入进行合适的转义和过滤
  • 使用安全的模板引擎或自动转义函数
  • 使用HTTP头部中的Content Security Policy (CSP),CSP 通过白名单机制,限制浏览器加载内容的来源,从而有效地减少跨站脚本攻击(XSS)、数据注入和点击劫持等安全风险。

3. 跨站请求伪造 (CSRF):攻击者利用受害者的登录状态,向服务器发送恶意请求,代替用户完成指定的操作。

防护方法:

  • 使用CSRF Token进行验证
  • 验证请求来源
  • 验证HTTP Referer头
// 使用CSRF Token进行验证
app.use((req, res, next) => {
  res.locals.csrfToken = generateCSRFToken();
  next();
});

// 验证请求来源
if (req.headers.origin !== 'https://example.com') {
  // 请求不是来自预期的来源,拒绝处理
}

// 验证HTTP Referer头
if (req.headers.referer !== 'https://example.com/') {
  // 请求不是来自预期的来源,拒绝处理
}

4. 文件上传漏洞:攻击者上传恶意文件到服务器,可能会获取服务器权限或执行恶意代码。

防范方法

  • 限制上传文件类型和大小,检查文件内容,设置文件存储路径等。

5. 点击劫持:攻击者将目标网站隐藏在一个透明的iframe中,并诱使用户点击看似无害的内容,实际上触发了隐藏的目标网站操作。

防护方法:

  • 使用X-Frame-Options响应头,防止网页被嵌入到iframe中
  • 使用Content Security Policy (CSP)
  • 使用Framebusting脚本防止被嵌套。

6. 不安全的重定向和跳转

防护方法:

  • 对重定向URL进行白名单验证
  • 验证跳转请求的合法性
  • 使用HTTP Only和Secure标志的Cookie

7.会话劫持和会话固定
会话劫持是攻击者盗取用户的会话标识来冒充用户,利用这个标识冒充用户与服务器进行通信,从而获得用户权限的攻击行为。

会话固定是攻击者预先设定一个会话标识,并诱使受害者使用这个标识登录,从而获得对用户会话的控制。
防护方法:

  • 使用安全的会话管理机制(如使用HTTPS、使用HTTP Only和Secure标志的Cookie)
  • 生成随机且复杂的会话ID
  • 定期更新会话ID

14 事件模型

事件模型包括三个阶段:捕获阶段、目标阶段和冒泡阶段。

1. 捕获阶段(Capture Phase):事件从最外层的父节点开始向下传递,直到达到目标元素。

2. 目标阶段(Target Phase):事件到达目标元素本身,触发目标元素上的事件处理程序。如果事件有多个处理程序绑定在目标元素上,它们会按照添加的顺序依次执行。

3. 冒泡阶段(Bubble Phase):事件从目标元素开始向上冒泡,传递到父节点,直到传递到最外层的父节点或根节点。

addEventListener第三个参数用于指定事件监听器是在捕获阶段(true)还是冒泡阶段(false,默认值)被触发。

// 注册一个在捕获阶段触发的事件监听器
document.body.addEventListener('click', function(event) {
  console.log('Body captured event');
}, true); // 第三个参数为 true

// 注册一个在冒泡阶段触发的事件监听器
document.getElementById('myButton').addEventListener('click', function(event) {
  console.log('Button bubbled event');
}, false); // 第三个参数为 false 或者省略

15 offsetWidth/offsetHeight,clientWidth/clientHeight与scrollWidth/scrollHeight的区别

offsetWidth/offsetHeight返回值包含content + padding + border + 滚动条,效果与e.getBoundingClientRect()相同
clientWidth/clientHeight返回值只包含content + padding,如果有滚动条,也不包含滚动条
scrollWidth/scrollHeight返回值包含content + padding + 溢出内容的尺寸

16 常见兼容性问题?

1.浏览器的盒模型差异:不同浏览器对盒模型的解析存在差异,导致元素的尺寸计算不一致。可以使用CSS盒模型属性(box-sizing)来进行控制。
2.浏览器对CSS属性的支持差异:不同浏览器对CSS属性的支持程度不同,某些属性在某些浏览器中可能不起作用或解析不正确。需要使用CSS前缀(Vendor Prefix)或使用兼容性方案来处理。
3.JavaScript API的差异:不同浏览器对JavaScript API的支持存在差异,某些方法、属性或事件在某些浏览器中可能不可用或行为不同。需要进行兼容性检测并使用替代方案或进行特定的处理。
4.样式的兼容性:不同浏览器对样式的解析存在差异,可能导致页面显示不一致。需要针对不同浏览器进行样式的调整和优化。
5.图片格式的兼容性:不同浏览器对图片格式的支持存在差异,某些格式在某些浏览器中可能不被支持或显示异常。需要根据需求选择合适的图片格式,并进行兼容性处理。
6.事件处理的差异:不同浏览器对事件的处理存在差异,例如事件对象的属性、方法、坐标获取等方面。需要进行兼容性处理,使用合适的方法来获取事件相关信息。

17 说说你对promise的了解

定义:Promise是JavaScript中用于处理异步操作的一种机制,它可以更加优雅地处理回调地狱(callback
hell)问题,使得异步代码更易读、更易维护。

1.Promise 的三种状态

1. 待定(pending):初始状态,既没有被完成,也没有被拒绝。
2. 已完成(fulfilled):操作成功完成。
3. 已拒绝(rejected):操作失败。
  • 可以把 Promise看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject,将状态转变为 fulfilled 或者 rejected 状态,状态一旦改变就不能再次变化。
  • then 函数或catch函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例,所以它们可以继续被链式调用。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

2. Promise 的静态方法

  • all 方法
    • 语法: Promise.all(iterable)
    • 参数: 一个可迭代对象,如 Array
    • 描述: 此方法对于汇总多个promise的结果很有用,在 ES6 中可以将多个Promise.all异步请求并行操作,返回结果一般有下面两种情况。
      • 当所有结果成功返回时按照请求顺序返回成功结果。
        • 当其中有一个失败方法时,则进入失败方法
// 在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面需要同时发出请求进行页面渲染,这样用 `Promise.all` 来实现,看起来更清晰、一目了然。


//1.获取轮播数据列表
function getBannerList(){
  return new Promise((resolve,reject)=>{
      setTimeout(function(){
        resolve('轮播数据')
      },300) 
  })
}
//2.获取店铺列表
function getStoreList(){
  return new Promise((resolve,reject)=>{
    setTimeout(function(){
      resolve('店铺数据')
    },500)
  })
}
//3.获取分类列表
function getCategoryList(){
  return new Promise((resolve,reject)=>{
    setTimeout(function(){
      resolve('分类数据')
    },700)
  })
}
function initLoad(){ 
  Promise.all([getBannerList(),getStoreList(),getCategoryList()])
  .then(res=>{
    console.log(res) 
  }).catch(err=>{
    console.log(err)
  })
} 
initLoad()
  • allSettled 方法
    • Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,返回一个新的 Promise唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功。
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回结果:
// [
//    { status: 'fulfilled', value: 2 },
//    { status: 'rejected', reason: -1 }
// ]
  • any 方法
    • 语法: Promise.any(iterable)
    • 参数: iterable 可迭代的对象,例如 Array
    • 描述: any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fulfilled状态,则返回最先解决的 Promise 对象的值,最后 any返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态。
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const anyPromise = Promise.any([resolved, rejected]);
anyPromise.then(function (results) {
  console.log(results);
});
// 返回结果:
// 2
  • race 方法
    • 语法: Promise.any(iterable)
    • 参数: iterable 可迭代的对象,例如 Array
    • 描述: race方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数
//请求某个图片资源
function requestImg(){
  var p = new Promise(function(resolve, reject){
    var img = new Image();
    img.onload = function(){ resolve(img); }
    img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png';
  });
  return p;
}
//延时函数,用于给请求计时
function timeout(){
  var p = new Promise(function(resolve, reject){
    setTimeout(function(){ reject('图片请求超时'); }, 5000);
  });
  return p;
}
Promise.race([requestImg(), timeout()])
.then(function(results){
  console.log(results);
})
.catch(function(reason){
  console.log(reason);
});

// 从上面的代码中可以看出,采用 Promise 的方式来判断图片是否加载成功,也是针对 Promise.race 方法的一个比较好的业务场景

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
promise手写实现,面试够用版:

function myPromise(constructor){
    let self=this;
    self.status="pending" //定义状态改变前的初始状态
    self.value=undefined;//定义状态为resolved的时候的状态
    self.reason=undefined;//定义状态为rejected的时候的状态
    function resolve(value){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
       }
    }
    function reject(reason){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
       }
    }
    //捕获构造异常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}
// 定义链式调用的then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

这里你谈 promise的时候,除了将他解决的痛点以及常用的 API 之外,最好进行拓展把 eventloop 带进来好好讲一下,microtask(微任务)、macrotask(任务) 的执行顺序,如果看过 promise 源码,最好可以谈一谈 原生 Promise 是如何实现的。Promise 的关键点在于callback 的两个参数,一个是 resovle,一个是 reject。还有就是 Promise 的链式调用(Promise.then(),每一个 then 都是一个责任人)

18 JS的基本数据类型和引用数据类型

  • 基本数据类型:
    • undefined: 表示未定义或未初始化的值。
    • null: 表示空值或不存在的对象。
    • boolean: 表示逻辑上的truefalse
    • number: 表示数值,包括整数和浮点数。
    • string: 表示字符串。
    • symbol: 表示唯一的、不可变的值,通常用作对象的属性键。
    • BigInt:表示大于 2^53 - 1 的整数。
  • 引用数据类型:
    • object: 包括普通对象 {},数组 [],函数 function(),正则表达式 /regexp/,以及特殊对象如日期对象 new Date() 等。

区别和特点:

  • 存储方式:基本数据类型直接存储在即栈内存中,引用数据类型的值通常存储在堆内存中。
  • 复制行为:当复制基本数据类型时,会创建一个新的副本;当复制引用数据类型时,会复制引用的地址,因此两个变量指向同一对象。
  • 占用内存大小:不同类型的基本数据在内存中占用的空间大小是固定的。引用数据类型的大小不是固定的,取决于对象的属性数量和大小。
  • 函数参数传递:函数参数如果是基本数据类型,传递的是值的拷贝;如果是引用数据类型,传递的是引用的拷贝。

19 null,undefined 的区别

区别如下:
1.含义::null 用于表示一个空值或者不存在的对象。undefined 表示一个变量已经声明但尚未赋值,或者属性不存在等情况。
2.typeof 返回值:使用 typeof 操作符检查 null 类型时,会返回 “object”。使用 typeof 操作符检查 undefined 类型时,会返回 “undefined”。
3.使用场景:null 通常用于显式地赋值,表示一个变量目前不指向任何对象。undefined 通常在变量声明后由 JavaScript 自动赋予,表示变量尚未被赋值。在函数中,如果没有明确返回值,那么函数的返回值就是 undefined。

20 [“1”, “2”, “3”].map(parseInt) 答案是多少

parseInt(str, radix)

  • 解析一个字符串,并返回10进制整数
  • 第一个参数str,即要解析的字符串
  • 第二个参数radix,基数(进制),范围2-36 ,以radix进制的规则去解析str字符串。不合法导致解析失败
  • 如果没有传radix
    • str0x开头,则按照16进制处理
    • str0开头,则按照8进制处理(但是ES5取消了,可能还有一些老的浏览器使用)会按照10进制处理
    • 其他情况按照10进制处理
  • eslint会建议parseInt写第二个参数(是因为0开始的那个8进制写法不确定(如078),会按照10进制处理)
// 拆解
const arr = ["1", "2", "3"]
const res = arr.map((item,index,array)=>{
  // item: '1', index: 0
  // item: '2', index: 1
  // item: '3', index: 2
  return parseInt(item, index)
  // parseInt('1', 0) // 0相当没有传,按照10进制处理返回1 等价于parseInt('1')
  // parseInt('2', 1) // NaN 1不符合redix 2-36 的一个范围
  // parseInt('3', 2) // 2进制没有3 返回NaN
})

// 答案 [1, NaN, NaN] 

21 JSON 的了解

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它采用类似于 JavaScript 对象的键值对的方式来描述数据,易于阅读和编写,同时也便于机器解析和生成。

JSON具有以下特点:
1.简洁性:JSON格式的数据简洁,易于阅读和编写。
2.支持多种数据类型:JSON支持包括字符串、数字、布尔值、对象、数组和null在内的基本数据类型。
3.跨平台和语言:JSON是一种通用的数据交换格式,不依赖于特定的编程语言或平台,可以被各种编程语言解析和生成。

在 JavaScript 中,可以使用内置的JSON对象进行 JSON 字符串与 JavaScript 对象之间的转换。常用的方法有:

  • JSON.parse():将 JSON 字符串解析为 JavaScript 对象。
  • JSON.stringify():将 JavaScript 对象转换为 JSON 字符串。
// JSON字符串转换为JSON对象
var jsonString = '{"name": "poetry", "age": 28, "city": "shenzhen"}';
var jsonObj = JSON.parse(jsonString);

// JSON对象转换为JSON字符串
var obj = {name: "poetry", age: 28, city: "shenzhen"};
var jsonString = JSON.stringify(obj);

22 defer和async

defer
用于延迟脚本的执行,即脚本会被并行下载,但会等到整个文档解析完成后再执行。多个带有defer属性的脚本会按照它们在文档中的顺序执行。这样可以确保脚本在操作DOM之前加载,避免阻塞页面的渲染。需要注意的是,只有外部脚本(通过src属性引入的脚本)才能使用defer属性。

async
async 属性用于异步加载脚本,即脚本会被并行下载,并在下载完成后立即执行。多个带有async属性的脚本的执行顺序是不确定的,哪个脚本先下载完成就先执行。这样可以提高脚本的加载性能,但可能会导致脚本之间的依赖关系出现问题。同样,只有外部脚本才能使用async属性。

<script src="script1.js" defer></script>
<script src="script2.js" async></script>

选择使用defer还是async取决于脚本的加载和执行顺序的重要性。如果脚本之间有依赖关系,并且需要按照顺序执行,应使用defer。如果脚本之间没有依赖关系,且可以并行加载和执行,可以使用async来提高加载性能。

23 谈谈你对ES6的理解

ES6(ECMAScript 2015)是JavaScript的第六个主要版本,引入了许多新的语言特性和改进,以提升开发人员的效率和代码质量。以下是ES6的一些重要特性:

  1. 块级作用域:引入letconst关键字,允许在块级作用域中声明变量,解决了变量提升和作用域污染的问题。
  2. 箭头函数:使用箭头(=>)定义函数,简化了函数的书写,并且自动绑定了this
  3. 模板字符串:使用反引号(`)包裹字符串,可以在字符串中使用变量和表达式,实现更灵活的字符串拼接和格式化。
  4. 解构赋值:通过解构赋值语法,可以从数组或对象中提取值,并赋给对应的变量,简化了变量赋值的操作。
  5. 默认参数:函数可以定义默认参数值,简化了函数调用时传参的操作。
  6. 扩展运算符:使用三个点(...)进行数组和对象的展开操作,可以将一个数组或对象拆分为独立的元素,或者将多个数组或对象合并为一个。
  7. Promise:引入了Promise对象,用于更好地处理异步操作,解决了回调地狱的问题,并提供了更清晰的异步编程模式。
  8. 类和模块化:ES6引入了类的概念,可以使用class关键字定义类,实现了更接近传统面向对象编程的方式。同时,ES6还提供了模块化的支持,可以使用importexport语法导入和导出模块。
  9. 模块化:引入了模块化的概念,可以使用importexport语法导入和导出模块,提供了更好的代码组织和模块复用的方式。
  10. 迭代器和生成器:引入了迭代器和生成器的概念,可以通过自定义迭代器来遍历数据集合,并使用生成器函数来生成迭代器。
  11. 管道操作符:提案阶段的特性,引入了管道操作符(|>),可以将表达式的结果作为参数传递给下一个表达式,简化了函数调用和方法链的写法。

24 什么是面向对象编程及面向过程编程,它们的异同和优缺点

面向过程编程(Procedural Programming)和面向对象编程(Object-Oriented Programming)是两种不同的编程范式。

面向过程编程是一种以过程(函数、方法)为中心的编程方式,将程序看作是一系列的步骤,通过顺序执行这些步骤来解决问题。面向过程编程强调的是解决问题的步骤和算法,将问题划分为不同的子任务,通过函数的调用和数据的传递来实现任务之间的协作。

面向对象编程是一种以对象为中心的编程方式,将程序看作是一系列相互关联的对象,通过对象之间的交互和消息传递来解决问题。面向对象编程强调的是事物(对象)的抽象、封装、继承和多态性。

异同和优缺点:

  • 异同:
    • 异同点在于编程的思维方式和组织代码的方式不同,面向过程注重解决问题的步骤和算法,面向对象注重对象和对象之间的关系和交互。
    • 面向过程将问题分解为不同的函数来实现,而面向对象将问题抽象为对象,通过对象的方法来实现功能。
  • 优点:
    • 面向过程编程的优点包括:简单、直观、执行效率高。
    • 面向对象编程的优点包括:可重用性高、易于扩展和维护、代码更加模块化和灵活。
  • 缺点:
    • 面向过程编程的缺点包括:代码重复性高、维护困难、扩展性差。
    • 面向对象编程的缺点包括:复杂性高、学习曲线陡

25 如何通过JS判断一个数组

1.Array.isArray() 方法:

let isArray = Array.isArray(variable);

2.instanceof Array 操作符::instanceof 操作符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

let isArray = variable instanceof Array;

3.Object.prototype.toString.call() 方法:

let isArray = Object.prototype.toString.call(variable) === '[object Array]';

4.检查 constructor 属性:如果数组是通过不同的 Array 构造函数创建的(例如,从不同的全局上下文或使用 Array.create),这种方法可能会失败。

let isArray = variable.constructor === Array;

26 谈一谈let与var的区别

1.作用域:

  • let声明的变量具有块级作用域,在块级作用域内定义的变量只在该块内有效。
  • var声明的变量没有块级作用域,它的作用域是函数级的或全局的。

2.变量提升:

  • let 声明的变量也会被提升,但是它们不会被初始化。在声明之前访问这些变量会导致 ReferenceError。具有暂时性死区。
  • 使用 var 声明的变量会存在变量提升,可以在声明之前使用。

3. 重复声明

  • 使用 let 声明的变量不允许重复声明,重复声明会导致报错。
  • 使用 var 声明的变量允许重复声明,不会报错,后面的声明会覆盖前面的声明。

4.全局对象属性

  • let声明的变量不会成为全局对象的属性
  • var声明的变量会成为全局对象的属性

5.循环中的区别

  • 使用 let 声明的变量在循环体内部具有块级作用域,每次迭代都会创建一个新的变量。
  • 使用 var 声明的变量在循环体内部没有块级作用域,变量是函数级的或全局的。

27 谈一谈你理解的函数式编程

函数式编程是一种编程范式,它将计算视为数学函数的求值,函数式编程具有以下特点:

  1. 纯函数(Pure Functions):函数的输出只由输入决定,不会产生副作用,即对同样的输入始终返回相同的输出。纯函数不会修改传入的参数,也不会改变外部状态,使得代码更加可预测和易于测试。
  2. 不可变性(Immutability):数据一旦创建就不能被修改,任何对数据的改变都会创建一个新的数据副本。这种不可变性使得代码更加安全,避免了一些潜在的错误。
  3. 高阶函数(Higher-Order Functions):函数可以作为参数传递给其他函数,也可以作为返回值返回。这种高阶函数的能力可以用来进行函数的组合、封装和抽象,提高代码的复用性和可读性。
  4. 函数组合(Function Composition):通过将多个函数组合成一个新的函数,可以实现更复杂的逻辑。函数组合可以通过函数的返回值作为参数传递给另一个函数,将多个函数连接起来形成一个函数链。
  5. 惰性计算(Lazy Evaluation):只在需要的时候才进行计算,避免不必要的计算。这种惰性计算可以提高程序的性能和效率。

28 谈一谈箭头函数与普通函数的区别?

  1. this指向: 箭头函数没有自己的this,它会捕获所在上下文的this值。而普通函数的this是在运行时确定的,根据调用方式决定。

  2. 不可作为构造函数: 箭头函数不能使用new关键字来创建实例,它没有自己的prototype属性,无法进行实例化。

  3. arguments对象: 箭头函数没有自己的arguments对象,可以使用Rest参数来代替。

  4. yield命令: 箭头函数不能用作Generator函数,无法使用yield命令进行函数的暂停和恢复。

29 JS 数组和对象的遍历方式,以及几种方式的比较

数组的遍历方式:

1. for循环
2. forEach方法
3. for…of循环
for…of 语句执行一个循环,该循环处理来自可迭代对象的值序列。

const array = [1, 2, 3];
array.forEach((element) => {
  console.log(element);
});

4. map方法

对象的遍历方式:

1. for…in循环
for…in 语句迭代一个对象的所有可枚举字符串属性(除 Symbol 以外),包括继承的可枚举属性。

const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key, obj[key]);
}

2. Object.keys方法结合forEach方法
Object.keys() 静态方法返回一个由给定对象自身的可枚举的字符串键属性名组成的数组。

const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).forEach((key) => {
  console.log(key, obj[key]);
});

3. Object.entries方法结合forEach方法
Object.entries() 静态方法返回一个数组,包含给定对象自有的可枚举字符串键属性的键值对。

const obj = { a: 1, b: 2, c: 3 };
Object.entries(obj).forEach(([key, value]) => {
  console.log(key, value);
});

30 let var const区别

1.作用域

  • let 和 const 都具有块级作用域,var 只有函数作用域和全局作用域

2.可变性

  • let 和 var 允许变量的值被重新赋值。
  • const 声明后不可再被赋值,但并不意味着其值是不可变的,如果 const 声明的是一个对象,那么对象内部的属性是可以被修改的。`

3.变量提升

  • var 声明的变量会被提升到其所在函数或全局作用域的顶部,但是初始化(赋值)不会提升。
  • let 和 const 声明的变量也会被提升,但是它们不会被初始化。在声明之前访问这些变量会导致 ReferenceError。具有暂时性死区。

31 怎样添加、移除、移动、复制、创建和查找节点

创建新节点

document.createElement(tagName); // 创建一个指定标签名的元素节点
document.createTextNode(text); // 创建一个包含指定文本的文本节点
document.createDocumentFragment(); // 创建一个空的文档片段节点

添加节点

parentNode.appendChild(); //将新创建的节点添加到父节点的子节点列表的末尾。
parentNode.insertBefore(newNode, referenceNode); //将新节点插入到参考节点之前。

移除节点

parentNode.removeChild(node); //从其父节点中删除一个子节点。
node.remove(); //直接在要删除的节点上调用,这会从DOM中移除该节点。

查找节点

document.getElementById(id); // 通过元素的ID查找节点。
document.getElementsByTagName(tagName); // 通过标签名查找一组节点。
document.getElementsByClassName(className); // 通过类名查找一组节点。
document.querySelector(selector); // 通过CSS选择器查找第一个匹配的节点。
document.querySelectorAll(selector); // 通过CSS选择器查找所有匹配的节点。

替换节点

parentNode.replaceChild(newNode, oldNode); // 用新节点替换指定的旧节点

复制节点

node.cloneNode(true); // 复制一个节点及其所有后代节点(true 表示深拷贝)。
node.cloneNode(false); // 复制一个节点,但不包括其后代节点(false 表示浅拷贝)。

32 正则表达式

33 数组去重方法总结

利用ES6 Set去重(ES6中最常用)

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = [...new Set(arr)];

使用 indexOf 方法
通过遍历数组并使用 indexOf 方法检查元素是否已经存在于新数组中来去重。

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
  if (uniqueArr.indexOf(arr[i]) === -1) {
    uniqueArr.push(arr[i]);
  }
}

使用 filter 方法

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);

使用 includes 方法

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
  if (!uniqueArr.includes(arr[i])) {
    uniqueArr.push(arr[i]);
  }
}

34 项目做过哪些性能优化?

这是一些常见的性能优化措施,可以在项目中采取来提升网页的加载速度和性能。以下是一些常见的性能优化措施:

  1. 减少 HTTP 请求数:合并和压缩 CSS、JavaScript 文件,使用雪碧图、字体图标等减少图片请求,减少不必要的资源请求。
  2. 减少 DNS 查询:减少使用不同的域名,以减少 DNS 查询次数。
  3. 使用 CDN:将静态资源部署到 CDN 上,提供更快的访问速度。 <script src="https://cdn.example.com/script.js"></script>
  4. 避免重定向:确保网页没有多余的重定向,减少额外的网络请求。
  5. 图片懒加载:延迟加载图片,只有当图片进入可视区域时再进行加载。
<img src="placeholder.jpg" data-src="image.jpg" class="lazyload">
<script src="lazyload.js"></script>
  1. 减少 DOM 元素数量:优化页面结构,减少 DOM 元素的数量,提升渲染性能。
  2. 减少 DOM 操作:避免频繁的 DOM 操作,合并操作或使用 DocumentFragment 进行批量操作。
var container = document.getElementById("container");
var fragment = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
  var div = document.createElement("div");
  div.innerText = "Element " + i;
  fragment.appendChild(div);
}
container.appendChild(fragment);
  1. 使用外部 JavaScript 和 CSS:将 JavaScript 和 CSS 代码外部化,利用浏览器缓存机制提高页面加载速度。
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
  1. 压缩文件:压缩 JavaScript、CSS、字体、图片等静态资源文件,减小文件大小。
  2. 优化 CSS Sprite:将多个小图标合并为一个大图,并通过 CSS 进行定位,减少图片请求。
.icon {
  background-image: url("sprite.png");
  background-position: -10px -20px;
  width: 20px;
  height: 20px;
}
  1. 使用 iconfont:将图标字体作为替代图像,减少图片请求并提高渲染性能。
<i class="iconfont">&#xe001;</i>
  1. 字体裁剪:只加载页面上实际使用的字体字符,减少字体文件的大小。需要使用字体工具(如FontelloIcoMoon等)进行裁剪
  2. 多域名分发:将网站的内容划分到不同的域名下,以提高并发请求的能力。需要在项目中配置不同的域名或子域名
  3. 减少使用 iframe:避免频繁使用 iframe,因为它们会增加额外的网络请求和页面加载时间。
  4. 避免图片 src 为空:确保 img 标签的 src 属性不为空,避免浏览器发送不必要的请求。
  5. 把样式表放在 link 中:避免使用内联样式,将样式表放在 link 标签中,使浏览器可以并行加载样式和内容。
  6. 把 JavaScript 放在页面底部:将 JavaScript 脚本放在页面底部,使页面内容可以先加载完毕,提升用户体验。

webpack性能优化

  1. 使用生产模式(production mode):在Webpack配置中设置modeproduction,这将启用许多内置的优化功能,例如代码压缩、作用域提升等。
  2. 代码分割(Code Splitting):使用Webpack的代码分割功能,将代码拆分为多个小块,按需加载,避免打包一个巨大的文件。
  3. 懒加载(Lazy Loading):使用动态导入(Dynamic Import)或import()函数,按需加载模块,在需要时才加载相关代码。
  4. Tree Shaking:通过配置Webpack的optimization选项,启用sideEffectsusedExports,以消除未使用的代码(dead code)。
  5. 缓存:使用Webpack的chunkhashcontenthash生成文件名,实现缓存机制,利用浏览器缓存已经加载的文件。
  6. 并行处理(Parallel Processing):使用thread-loaderHappyPack插件,将Webpack的构建过程多线程化,加速构建速度。
  7. 使用缩小作用域(Narrowing the Scope):通过配置Webpack的resolve选项,缩小模块解析的范围,减少不必要的查找。
  8. 使用外部依赖(External Dependencies):将一些稳定的、不经常修改的库或框架通过externals配置排除,使用CDN引入,减少打包体积。
  9. 使用插件和加载器(Plugins and Loaders):选择高效的插件和加载器,合理配置它们的选项,以优化构建过程和资源处理。
  10. 使用Webpack Bundle Analyzer:使用Webpack Bundle Analyzer工具分析打包后的文件,查找体积较大、冗余或不必要的模块,进行进一步优化。

这些是一些常见的Webpack性能优化技巧,可以根据具体项目需求进行选择和配置,以提升构建速度和优化输出结果。

React的性能优化策略:

  1. 使用React.memo()PureComponent:对于函数组件,可以使用React.memo()函数或继承PureComponent类来进行浅比较,避免不必要的重新渲染
  2. 使用key属性进行列表优化:在渲染列表时,为每个列表项提供唯一的key属性,以帮助React更有效地更新和重用组件
  3. 使用shouldComponentUpdateReact.memo()进行组件渲染控制:在类组件中,可以通过实现shouldComponentUpdate生命周期方法来控制组件的重新渲染。对于函数组件,可以使用React.memo()包裹组件并传递自定义的比较函数
  4. 懒加载组件:对于较大的组件或页面,可以使用React.lazy()Suspense组件进行按需加载,减少初始加载时间
  5. 使用虚拟化列表:对于长列表或大型数据集,可以使用虚拟化列表库(如react-virtualizedreact-window)来仅渲染可见部分,减少DOM操作和内存占用
  6. 使用Memoization进行计算的缓存:通过使用Memoization技术,可以将计算结果缓存起来,避免重复计算,提高性能。可以使用Memoization库(如reselect)来实现
  7. 使用React Profiler进行性能分析:React Profiler是React提供的性能分析工具,可以帮助定位应用中的性能瓶颈,并进行优化
  8. 使用ESLint和代码分析工具:通过使用ESLint等代码规范工具和静态代码分析工具,可以发现潜在的性能问题和优化机会,并进行相应的调整

35 浏览器缓存

浏览器的缓存机制指的是当用户访问网站时,浏览器会将这些资源存储在本地缓存中。如果在资源的有效时间内,发起了对这个资源的再一次请求,那么浏览器会直接使用缓存的副本,而不是向服务器发起请求。这样可以加快页面加载速度,减少服务器的负担。

web 资源的缓存策略一般由服务器来指定,可以分为两种,浏览器缓存分为强缓存和协商缓存。

当客户端请求某个资源时,获取缓存的流程如下:

  • 先根据这个资源的一些 http header 判断它是否命中强缓存,如果命中,则直接从本地获取缓存资源,不会发请求到服务器( 如果响应头中包含 Cache-Control 或 Expires 字段,并且符合缓存规则(例如 max-age 值大于当前时间),则说明资源命中了强缓存。);
  • +当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些request header验证这个资源是否命中协商缓存,称为http再验证,如果命中,服务器将请求返回,但不返回资源,而是告诉客户端直接从缓存中获取,客户端收到返回后就会从缓存中获取资源;
  • 强缓存和协商缓存共同之处在于,如果命中缓存,服务器都不会返回资源; 区别是,强缓存不对发送请求到服务器,但协商缓存会。
  • 当协商缓存也没命中时,服务器就会将资源发送回客户端。
  • ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;

强缓存

强缓存是指浏览器在请求资源时,首先根据缓存规则判断是否可以直接从本地缓存中获取资源,而不需要发送请求到服务器。强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。

  • Expires(该字段是 http1.0 时的规范,值为一个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间):服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。
  • Cache-Control:
    • max-age(该字段是 http1.1的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期,它的值单位为秒): max-age 来指定资源能够被缓存的时间的大小,这是一个相对的时间,它会根据这个时间的大小和资源第一次请求时的时间来计算出资源过期的时间,因此相对于 Expires来说,这种方式更加有效一些。
    • no-cache:表示协商缓存,每次请求还是会和服务器去比对资源有没有修改(也就是拿ETag或者Last-Modified进行比较),如果资源没改变,则直接返回304状态码(Not Modified),说明无需再次传输请求的内容,也就是说可以使用缓存的内容;如果资源改变,则返回200状态码,并且返回新的资源;
    • no-store:指示不要缓存请求或响应的任何部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

协商缓存

当强缓存失效时,浏览器将使用协商缓存。使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。

  • Last-Modified:服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间。
  • If-Modified-Since:当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。
  • ETag:因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。
  • If-None-Match:在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified的方式更加精确。

当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。

强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。

36 深浅拷贝

浅拷贝

浅拷贝只是复制了对象的第一层属性,如果对象内部包含对象或数组,那么这些内部结构仍然是通过引用共享的。

浅拷贝对象有下面的方法

  • 使用 Object.assign() 方法
let obj1 = { a: 1, b: 2 };
let obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 1, b: 2 }
  • 使用展开运算符
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1 };
console.log(obj2); // { a: 1, b: 2 }

浅拷贝数组有下面的方法

  • Array.prototype.slice
let originalArray = [1, 2, 3];
let shallowCopyArray = originalArray.slice();
  • 扩展运算符(Spread Operator)
let shallowCopy = [...originalArray];

深拷贝
深拷贝会创建一个全新的对象,并且递归复制所有子对象和数组,使得原始对象和副本完全独立。

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  let clone = Array.isArray(obj) ? [] : {};
  
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key]);
    }
  }
  
  return clone;
}

let obj1 = {
  a: 1,
  b: { c: 2 }
};

let obj2 = deepClone(obj1);
obj2.b.c = 3;

console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 3
  • 使用 JSON.parse(JSON.stringify()) 实现深拷贝
let obj1 = {
  a: 1,
  b: { c: 2 }
};

let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;

console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 3

使用 JSON.parse(JSON.stringify()) 实现深拷贝也是有局限性的

  • 会忽略 undefined:JSON.stringify() 方法在序列化对象时会忽略 undefined 属性,序列化后的结果不包含该属性。
  • 不能序列化函数:JSON.stringify() 方法在序列化对象时会忽略函数属性,因为函数不符合 JSON 格式的数据类型。经过序列化和反序列化后,函数属性会丢失。
  • 不能解决循环引用的对象:如果对象存在循环引用,即对象内部包含对自身的引用,JSON.stringify() 方法无法正确处理,会导致循环引用的属性被序列化为 null
  • 无法处理特殊对象:JSON.stringify() 方法无法序列化某些特殊对象,如 Date 对象、正则表达式、Map、Set 等,它们在序列化过程中会转换成空对象。

37 防抖/节流

防抖(Debounce)
防抖函数原理:防抖是指在事件被触发后等待一定的延迟时间,如果在这段延迟时间内事件又被重新触发,则重新开始计算延迟时间。只有当指定的时间间隔内没有再次触发事件时,才会执行函数。

function debounce(func, delay) {
    let timer = null;
    
    return function(...args) {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// 用法示例
const debouncedFunc = debounce(() => {
    console.log('Debounced function executed');
}, 300);

// 在事件处理器中使用防抖函数
input.addEventListener('input', debouncedFunc);

适用场景

  • 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好
  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

节流(Throttle)

节流是指在指定的时间间隔内,无论事件触发多少次,只有第一次触发的函数会执行,之后的触发都会被忽略,直到下一个时间间隔。

节流同样适用于滚动事件、窗口大小调整等频繁触发的事件,可以保证在一个时间段内函数只执行一次。

function throttle(func, delay) {
    let timer = null;
    
    return function(...args) {
        if (!timer) {
            timer = setTimeout(() => {
                func.apply(this, args);
                timer = null;
            }, delay);
        }
    };
}

// 用法示例
const throttledFunc = throttle(() => {
    console.log('Throttled function executed');
}, 300);

// 在事件处理器中使用节流函数
window.addEventListener('scroll', throttledFunc);

适用场景

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动。DOM 元素的拖拽功能实现(mousemove)
  • 缩放场景:监控浏览器resize
  • 滚动场景:监听滚动scroll事件判断是否到页面底部自动加载更多
  • 动画场景:避免短时间内多次触发动画引起性能问题

38 谈谈变量提升

在JavaScript中,变量提升(Variable Hoisting)是指在代码执行前,JavaScript引擎会先对变量和函数声明进行处理,将它们提升到各自作用域的顶部。这意味着变量和函数可以在声明之前使用,而不会抛出引用错误(ReferenceError)。

变量提升的规则

  • 函数声明提升
    • 使用函数声明(Function Declaration)定义的函数会被提升到它们所在的作用域顶部。这意味着可以在函数声明之前调用这些函数。函数声明总是优先于变量声明,即使变量声明在函数声明之前,变量声明会被函数声明覆盖
  • 变量声明提升
    • 使用var声明的变量会被提升到函数或全局作用域的顶部,但是初始化(赋值)不会提升。如果在声明之前访问变量,其值会是undefined。
  • let 和 const 提升
    • 变量声明会被提升到作用域的顶部,但是不会初始化。它们会保持在暂时性死区,直到它们的声明被执行。

暂时性死区(TDZ)

在代码执行到达变量声明的位置之前,let 和 const 声明的变量是不可见的,这个时间段就是所谓的“暂时性死区”。在死区内访问这些变量将导致运行时错误。

38 什么是单线程,和异步的关系

在 JavaScript 中,单线程指的是 JavaScript 引擎在执行代码时只有一个主线程,也就是说一次只能执行一条指令。这意味着 JavaScript 代码是按照顺序执行的,前一段代码执行完成后才会执行下一段代码。

  • 异步是一种编程模型,用于处理非阻塞的操作。在 JavaScript 中,异步编程可以通过回调函数、Promiseasync/await 等方式来实现。异步操作不会阻塞主线程的执行,从而提高了程序的响应性能和用户体验。
  • 异步的关系与单线程密切相关,因为 JavaScript 是单线程的,如果所有的操作都是同步的,那么一旦遇到一个耗时的操作,比如网络请求或文件读取,整个程序都会被阻塞,用户界面也会停止响应,导致用户体验差。
  • 通过使用异步编程模型,可以将耗时的操作委托给其他线程或进程来处理,使得主线程可以继续执行其他任务,提高了程序的并发性和响应性。当异步操作完成后,通过回调函数或 Promise 的方式通知主线程,主线程再执行相应的回调逻辑。

总结一下:

  • JavaScript 是单线程的,只有一个主线程用于执行代码。
  • 异步编程是一种处理非阻塞操作的方式,提高程序的响应性能和用户体验。
  • 异步操作可以将耗时的任务委托给其他线程或进程处理,主线程继续执行其他任务。
  • 异步操作完成后通过回调函数或 Promise 的方式通知主线程。

39 为什么要有同源限制?

同源策略指的是:协议,域名,端口相同,同源策略是一种安全协议

同源限制是为了保护用户的隐私和安全而存在的。同源限制的主要目的是为了隔离潜在的恶意文件,防止跨站脚本(XSS)攻击、CSRF攻击、会话劫持等安全问题。

同源策略通过限制来自不同源的网页之间的交互,确保只有同源的网页可以相互访问彼此的资源。

40 JavaScript的组成

  1. ECMAScript(核心):ECMAScript是JavaScript的基础,定义了语言的语法、类型、语句、关键字等。它规定了JavaScript的基本语法、数据类型、函数、运算符、控制流等核心特性,并提供了对数组、对象、字符串、正则表达式等的操作方法和功能。ECMAScript的版本以ES6(ES2015)为基准,随着时间的推移,新版本的ECMAScript引入了更多的语言特性和功能。
  2. DOM(文档对象模型):DOM是一种表示和操作HTML、XML文档的接口。它定义了文档的结构、属性和方法,允许开发者通过JavaScript来访问和修改网页的内容、结构和样式。DOM将文档表示为一个树形结构,其中每个节点代表文档中的一个元素、属性、文本等。开发者可以使用DOM提供的API对这些节点进行增删改查操作,实现动态更新和交互效果。
  3. BOM(浏览器对象模型):BOM是一种提供了与浏览器窗口进行交互的接口。它提供了访问浏览器窗口、处理窗口尺寸、导航历史、处理Cookie、发送HTTP请求等功能。BOM中的对象包括windownavigatorlocationhistoryscreen等,开发者可以使用这些对象来控制浏览器的行为和获取相关信息。

这三部分共同构成了JavaScript的整体,使其成为一种强大的编程语言,能够在网页中实现丰富的交互和动态效果。

41 script 的位置是否会影响首屏显示时间

  • script 的位置对首屏显示时间有影响。虽然浏览器在解析 HTML 生成 DOM 过程中,js 文件的下载是并行的,不需要 DOM 处理到 script 节点,但是脚本的执行会阻塞页面的解析和渲染。
  • 当浏览器遇到 script 标签时,会暂停解析 HTML,开始下载并执行脚本。只有脚本执行完毕后,浏览器才会继续解析和渲染页面。
  • 如果 script 标签放在 <head> 标签中,那么脚本的下载和执行会先于页面的渲染,这样会延迟首屏显示的开始时间。
  • 为了提高首屏显示时间,一般建议将 script 标签放在 <body> 标签底部,在大部分内容都已经显示出来后再加载和执行脚本,这样可以让页面尽快呈现给用户,提升用户体验。
  • 另外,可以使用异步加载的方式(如将 script 标签添加 async 属性)或延迟加载的方式(如将 script 标签添加 defer 属性),来减少脚本对页面加载的阻塞影响。这样可以在不阻塞页面渲染的情况下加载和执行脚本,加快首屏显示的完成时间。

42 Javascript垃圾回收方法

现代的 JavaScript 引擎会使用标记清除(mark and sweep)算法作为主要的垃圾回收方法。引用计数(reference counting)在某些老旧的 JavaScript 引擎中可能会被使用。

标记清除(mark and sweep)算法

  • 垃圾回收器会在运行时给存储在内存中的所有变量加上标记。假设内存中所有对象都是垃圾,全标记为0
  • 垃圾回收器会从根对象开始,递归遍历所有的引用,标记它们为“进入环境”,把不是垃圾的节点改成1。
  • 在遍历完成后,垃圾回收器会对未被标记的变量进行清除,即将其回收内存空间。
  • 被清除的内存空间将被重新分配给后续的变量使用,把所有内存中对象标记修改为0,等待下一轮垃圾回收。

优点

  • 标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单。

缺点

  • 内存碎片化:空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
  • 分配速度慢:因为即便是使用 First-fit 策略(找到大于等于 size 的块立即返回),其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢在分配内存时。

归根结底,标记清除算法的缺点在于清除之后剩余的对象位置不变而导致的空闲内存不连续,所以只要解决这一点,两个缺点都可以完美解决了
标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。

V8对GC的优化

V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收

新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量,而老生代的对象为存活事件较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大。
对于新老两块内存区域的垃圾回收,V8 采用了两个垃圾回收器来管控,我们暂且叫做新生代垃圾回收器和老生代垃圾回收器

分代式垃圾回收
我们上面所说的垃圾清理算法在每次垃圾回收时都要检查内存中所有的对象,这样的话对于一些大、老、存活时间长的对象来说同新、小、存活时间短的对象一个频率的检查很不好,因为前者需要时间长并且不需要频繁进行清理,后者恰好相反,怎么优化这点呢???分代式就来了

新生代垃圾回收
新生代对象是通过一个名为 Scavenge 的算法进行垃圾回收,在 Scavenge算法 的具体实现中,主要采用了一种复制式的方法即 Cheney算法 ,我们细细道来
Cheney算法 中将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区,一个是处于闲置状态的空间我们称之为 空闲区,如下图所示
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作
当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。最后进行角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区
当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理
另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge 回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配。

老生代垃圾回收
相比于新生代,老生代的垃圾回收就比较容易理解了,上面我们说过,对于大多数占用空间大、存活时间长的对象会被分配到老生代里,因为老生代中的对象通常比较大,如果再如新生代一般分区然后复制来复制去就会非常耗时,从而导致回收执行效率不高,所以老生代垃圾回收器来管理其垃圾回收执行,它的整个流程就采用的就是上文所说的标记清除算法了
首先是标记阶段,从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象
清除阶段老生代垃圾回收器会直接将非活动对象,也就是数据清理掉
前面我们也提过,标记清除算法在清除后会产生大量不连续的内存碎片,过多的碎片会导致大对象无法分配到足够的连续内存,而 V8 中就采用了我们上文中说的标记整理算法来解决这一问题来优化空间。

之后再补充
https://juejin.cn/post/6981588276356317214

43 请解释一下 JavaScript 的同源策略

同源策略(Same-Origin Policy)是浏览器中一种重要的安全机制,用于限制来自不同源(协议、域名、端口)的脚本对当前文档的访问权限。同源策略的作用是保护用户的信息安全,防止恶意网站获取敏感数据或进行跨站攻击。
同源策略限制了以下行为:

  1. 脚本访问跨源文档的 DOM:通过脚本在页面中嵌入的 iframe 元素加载的跨源文档无法通过脚本访问其 DOM,除非目标文档明确允许。
  2. 脚本读取跨源文档的内容:通过脚本在页面中嵌入的 iframe 元素加载的跨源文档无法通过脚本读取其内容,包括读取属性、执行方法等。
  3. 脚本发送跨源 AJAX 请求:脚本无法直接发送跨源的 AJAX 请求,只能向同源的服务器发送请求。
  4. CookieLocalStorageIndexDB 的限制:跨源的脚本无法访问其他源的 CookieLocalStorageIndexDB 数据。

同源策略的存在使得浏览器可以更好地保护用户的隐私和安全。然而,也有一些场景需要进行跨域访问,例如使用 JSONPCORS、代理服务器等方式来实现跨域请求。

需要注意的是,同源策略仅在浏览器中执行,不会限制服务器之间的通信,服务器可以自由地进行跨域访问。

44 说说Web Worker

Web Worker是HTML5提供的一个API,它允许JavaScript运行在后台线程中,独立于主线程,从而实现多线程编程,提高Web应用程序的性能和响应速度。

Web Worker的特点:

  • 独立于主线程

    • Web Worker可以在后台线程中运行,不会阻塞主线程的执行,从而提高Web应用程序的响应速度。
  • 可以并行执行

    • Web Worker的多线程模型可以使得多个线程并行执行,从而提高Web应用程序的性能。
  • 不能直接访问DOM和BOM

    • Web Worker不能直接访问DOM和BOM,因为这些API都是基于浏览器的主线程实现的,但是Web Worker可以通过postMessage方法和onmessage事件与主线程进行通信,从而实现数据的传递和交互。
  • 可以引入外部脚本

    • Web Worker可以通过importScripts方法引入外部脚本,从而扩展其功能。

使用场景

  • 大量计算密集型任务

    • Web Worker适合用于执行大量计算密集型的任务,如图像处理、音视频编解码等。
  • 后台数据处理

    • Web Worker可以在后台线程中处理数据,从而不影响主线程的执行。例如,可以使用Web Worker来处理数据的压缩、解压、加密等操作。
  • 异步网络请求

    • 使用Web Worker可以在后台线程中执行异步网络请求,从而避免阻塞主线程。例如,可以使用Web Worker来处理大量的WebSocket连接。

注意点

  • worker 运行在另一个全局上下文中,不同于当前的window。因此,在 Worker 内通过 window 获取全局作用域(而不是self)将返回错误。
  • 在 worker 内,不能直接操作 DOM 节点,也不能使用 window 对象的默认方法和属性。但是你可以使用大量 window 对象之下的东西,包括 WebSockets,以及 IndexedDB 等数据存储机制
<!DOCTYPE html>
<html>
<head>
	<title>Web Worker示例</title>
</head>
<body>
	<label for="n">输入n:</label>
	<input type="number" id="n">
	<button id="calculate">计算</button>
	<div id="result"></div>
	<script>
		//创建一个新的 worker 很简单。你需要做的是调用 Worker() 构造器,指定一个脚本的 URI 来执行 worker 线程
		const worker = new Worker('worker.js');
		// 获取HTML元素
		const nInput = document.getElementById('n');
		const calculateBtn = document.getElementById('calculate');
		const resultDiv = document.getElementById('result');
		// 监听Web Worker返回的消息
		worker.onmessage = function(event) {
			resultDiv.innerHTML = `斐波那契数列的第${nInput.value}项为${event.data}`;
		};
		// 点击计算按钮时,向Web Worker发送消息
		calculateBtn.onclick = function() {
			worker.postMessage(nInput.value);
		};
	</script>
</body>
</html>

worker.js

// 监听主线程发送的消息
onmessage = function(event) {
	const n = parseInt(event.data);
	const result = fibonacci(n);
	// 向主线程发送消息
	postMessage(result);
};
// 计算斐波那契数列的第n项
function fibonacci(n) {
	if (n == 0 || n == 1) {
		return n;
	} else {
		return fibonacci(n - 1) + fibonacci(n - 2);
	}
}

终止 worker

myWorker.terminate();

引入脚本与库
Worker 线程能够访问一个全局函数 importScripts() 来引入脚本,该函数接受 0 个或者多个 URI 作为参数来引入资源

importScripts(); /* 什么都不引入 */
importScripts("foo.js"); /* 只引入 "foo.js" */
importScripts("foo.js", "bar.js"); /* 引入两个脚本 */
importScripts("//example.com/hello.js"); /* 你可以从其他来源导入脚本 */

常用设计模式有哪些并举例使用场景

工厂模式

  • 使用场景:当需要根据不同的参数创建不同类型的对象时,可以使用工厂模式。例如,根据用户的选择创建不同类型的支付方式对象。
  • 优点:封装了对象的创建过程,客户端只需关注传入参数即可获取所需对象,降低了耦合度。
  • 缺点:增加了代码的复杂性,需要额外编写工厂方法。
const createUser = (role) => {
  if (role === 'admin') {
    return { role, access: 'full' };
  } else {
    return { role, access: 'limited' };
  }
};

const admin = createUser('admin');
const guest = createUser('guest');
console.log(admin.access); // full
console.log(guest.access); // limited

单例模式

  • 使用场景:当整个系统中只需要一个实例时,可以使用单例模式。例如单个配置对象或全局状态管理。
  • 优点:确保只有一个实例存在,提供了全局访问点,避免了重复创建实例。
  • 缺点:对扩展不友好,单例的实例化和使用耦合在一起。
const createSingleton = (() => {
  let instance;
  return () => {
    if (!instance) {
      instance = {};
    }
    return instance;
  };
})();

const instance1 = createSingleton();
const instance2 = createSingleton();
console.log(instance1 === instance2); // true

发布-订阅模式

  • 使用场景:当存在多个对象之间需要进行解耦的消息通信时,可以使用发布-订阅模式。例如,实现一个事件总线用于组件间的通信。
  • 优点:解耦了对象之间的通信,订阅者只需关注自己感兴趣的事件,发布者不需要关心具体的订阅者。
  • 缺点:容易造成内存泄漏,需要手动取消订阅,否则订阅者会一直存在。
const EventBus = {
  events: {},

  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },

  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  },

  unsubscribe(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
};

// 订阅事件
EventBus.subscribe('userLoggedIn', handleUserLoggedIn);

// 发布事件
EventBus.publish('userLoggedIn', { username: 'poetry' });

// 取消订阅事件
EventBus.unsubscribe('userLoggedIn', handleUserLoggedIn);

观察者模式

  • 使用场景:当一个对象的状态发生变化时,需要通知其他依赖该对象的对象进行相应操作时,可以使用观察者模式。例如,实现一个数据的双向绑定功能。
  • 优点:解耦了对象之间的关系,被观察者和观察者之间松耦合,可以动态添加和移除观察者。
  • 缺点:增加了对象之间的相互依赖关系,可能导致系统复杂度增加。
var observer = (function() {
  var observers = [];
  return {
    subscribe: function(fn) {
      observers.push(fn);
    },
    unsubscribe: function(fn) {
      observers = observers.filter(function(subscriber) {
        return subscriber !== fn;
      });
    },
    notify: function(data) {
      observers.forEach(function(fn) {
        fn(data);
      });
    }
  };
})();


const subject = createSubject();
const observer = (data) => console.log('Received:', data);
subject.subscribe(observer);
subject.notify('Form updated'); // Received: Form updated

装饰模式

  • 使用场景:当需要在不修改原始对象的情况下,动态地给对象添加额外的功能时,可以使用装饰模式。例如,给一个基本的组件添加日志记录或性能监测的功能。
  • 优点:遵循开放封闭原则,不需要修改原始对象的结构,可以灵活地添加或移除功能。
  • 缺点:增加了类的数量,可能导致类的层次复杂。
const baseFunction = () => 'Base Function';
const decorateFunction = (fn) => () => `${fn()} with Decoration`;

const decoratedFunction = decorateFunction(baseFunction);
console.log(decoratedFunction()); // Base Function with Decoration

策略模式

场景: 根据不同条件选择不同的算法或行为,例如不同的排序算法。

  • 使用场景:当需要根据不同的情况选择不同的算法或策略时,可以使用策略模式。例如,根据用户选择的不同排序方式对数据进行排序。
  • 优点:简化了条件语句的复杂度,将算法封装成独立的策略类,方便扩展和维护。
  • 缺点:增加了类的数量,可能导致类的层次复杂。
var strategyA = function() {
  console.log('Strategy A');
};
var strategyB = function() {
  console.log('Strategy B');
};
var context = {
  strategy: null,
  setStrategy: function(strategy) {
    this.strategy = strategy;
  },
  executeStrategy: function() {
    this.strategy();
  }
};
context.setStrategy(strategyA);
context.executeStrategy(); // Strategy A

代理模式

场景: 为某些对象添加额外功能或控制访问,例如在前端请求数据时进行缓存。

const createRealService = () => ({
  fetchData: () => 'Real Data'
});

const createProxyService = (realService) => {
  let cache;
  return {
    fetchData: () => {
      if (!cache) {
        cache = realService.fetchData();
      }
      return cache;
    }
  };
};

const realService = createRealService();
const proxy = createProxyService(realService);
console.log(proxy.fetchData()); // Real Data
console.log(proxy.fetchData()); // Cached Data

原型模式

  • 通过一个原型对象来共享公共属性和方法。
var personProto = {
  eyes: 2,
  nose: 1,
  ears: 2
};
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype = personProto;

适配器模式

场景: 使两个不兼容的接口能够协同工作,例如将旧版 API 适配到新版 API。

const oldSystem = () => ({
  oldMethod: () => 'Old System Data'
});

const newSystem = () => ({
  newMethod: () => 'New System Data'
});

const adapter = (newSystem) => ({
  oldMethod: () => newSystem.newMethod()
});

const newSystemInstance = newSystem();
const adaptedSystem = adapter(newSystemInstance);
console.log(adaptedSystem.oldMethod()); // New System Data

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值