由浅入深,66条JavaScript面试知识点

作者:Jake Zhang

来源:https://juejin.im/post/5ef8377f6fb9a07e693a6061

前言

最近一直在找工作,今年是真的难啊,但再难苟且的生活还要继续,饭碗还是要继续找的。在最近的面试中我一直在总结,每次面试回来也都会复盘,面了有七八家,也有那么几个offer,但终究不是很满意,总想再试试大一点的平台。下面是我这几天遇到的面试知识点。但今天主题是标题所写的66条JavaScript知识点,由浅入深,整理了一周,每(zhěng)天(lǐ)整(bù)理( yì)10条( qiú)左(diǎn)右(zàn),   希望对正在找工作的小伙伴有点帮助,文中如有表述不对,还请指出。

HTML&CSS:

  • 浏览器内核

  • 盒模型、flex布局、两/三栏布局、水平/垂直居中;

  • BFC、清除浮动;

  • css3动画、H5新特性。

JavaScript:

  • 继承、原型链、this指向、设计模式、call, apply, bind,;

  • new实现、防抖节流、let, var, const 区别、暂时性死区、event、loop;

  • promise使用及实现、promise并行执行和顺序执行;

  • async/await的优缺点;

  • 闭包、垃圾回收和内存泄漏、数组方法、数组乱序, 数组扁平化、事件委托、事件监听、事件模型

Vue:

  • vue数据双向绑定原理;

  • vue computed原理、computed和watch的区别;

  • vue编译器结构图、生命周期、vue组件通信;

  • mvvm模式、mvc模式理解;

  • vue dom diff、vuex、vue-router

网络:

  • HTTP1, HTTP2, HTTPS、常见的http状态码;

  • 浏览从输入网址到回车发生了什么;

  • 前端安全(CSRF、XSS)

  • 前端跨域、浏览器缓存、cookie, session, token, localstorage, sessionstorage;

  • TCP连接(三次握手, 四次挥手)

性能相关

  • 图片优化的方式

  • 500 张图片,如何实现预加载优化

  • 懒加载具体实现

  • 减少http请求的方式

另外更全面的面试题集我也在整理中,先给个预告图:

由浅入深,66条JavaScript面试知识点_第1张图片
下面进入正题:
  • 1. 介绍一下 js 的数据类型有哪些,值是如何存储的

  • 2. && 、 ||和!! 运算符分别能做什么

  • 3. js的数据类型的转换

  • 4. JS中数据类型的判断( typeof,instanceof,constructor,Object.prototype.toString.call()

  • 5. 介绍 js 有哪些内置对象?

  • 6. undefined 与 undeclared 的区别?

  • 7. null 和 undefined 的区别?

  • 8. {} 和 [] 的 valueOf 和 toString 的结果是什么?

  • 9. Javascript 的作用域和作用域链?

  • 10. javascript 创建对象的几种方式?

  • 11. JavaScript 继承的几种实现方式?

  • 12. 寄生式组合继承的实现?

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

  • 14. JavaScript 原型,原型链?有什么特点?

  • 15. js 获取原型的方法?

  • 16. 什么是闭包,为什么要用它?

  • 17. 什么是 DOM 和 BOM?

  • 18. 三种事件模型是什么?

  • 19. 事件委托是什么?

  • 20. 什么是事件传播?

  • 21. 什么是事件捕获?

  • 22. 什么是事件冒泡?

  • 23. DOM 操作——怎样添加、移除、移动、复制、创建和查找节点?

  • 24. js数组和对象有哪些原生方法,列举一下

  • 25. 常用的正则表达式

  • 26. Ajax 是什么? 如何创建一个 Ajax?

  • 27. js 延迟加载的方式有哪些?

  • 28. 谈谈你对模块化开发的理解?

  • 29. js 的几种模块规范?

  • 30. AMD和CMD 规范的区别?

  • 31. ES6 模块与 CommonJS 模块、AMD、CMD 的差异。

  • 32.   requireJS的核心原理是什么?

  • 33. 谈谈JS的运行机制

  • 34. arguments 的对象是什么?

  • 35. 为什么在调用这个函数时,代码中的`b`会变成一个全局变量?

  • 36.简单介绍一下V8引擎的垃圾回收机制

  • 37. 哪些操作会造成内存泄漏?

  • 38. ECMAScript 是什么?

  • 39. ECMAScript 2015(ES6)有哪些新特性?

  • 40. `var`,`let`和`const`的区别是什么?

  • 41. 什么是箭头函数?

  • 42. 什么是类?

  • 43. 什么是模板字符串?

  • 44. 什么是对象解构?

  • 45 什么是`Set`对象,它是如何工作的?

  • 46. 什么是Proxy?

————
高能预警分割线⚡—————

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

  • 48. 什么是函数式编程? JavaScript的哪些特性使其成为函数式语言的候选语言?

  • 49. 什么是高阶函数?

  • 50. 为什么函数被称为一等公民?

  • 51. 手动实现`Array.prototype.map 方法`

  • 52. 手动实现`Array.prototype.filter`方法

  • 53. 手动实现`Array.prototype.reduce`方法

  • 54. js的深浅拷贝

  • 55. 手写call、apply及bind函数

  • 56. 函数柯里化的实现

  • 57. js模拟new操作符的实现

  • 58. 什么是回调函数?回调函数有什么缺点

  • 59. Promise是什么,可以手写实现一下吗?

  • 60. `Iterator`是什么,有什么作用?

  • 61. `Generator`函数是什么,有什么作用?

  • 62. 什么是 `async/await`及其如何工作,有什么优缺点?

  • 63. instanceof的原理是什么,如何实现

  • 64. js的节流与防抖

  • 65. 什么是设计模式?

  • 66. 9种前端常见的设计模式

1. 介绍一下 js 的数据类型有哪些,值是如何存储的

具体可看我之前的文章:「前端料包」可能是最透彻的JavaScript数据类型详解

JavaScript一共有8种数据类型,其中有7种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示独一无二的值)和BigInt(es10新增);

1种引用数据类型——Object(Object本质上是由一组无序的名值对组成的)。里面包含 function、Array、Date等。JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。

原始数据类型:直接存储在(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。

引用数据类型:同时存储在(stack)和(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

2. && 、 ||和!! 运算符分别能做什么

  • && 叫逻辑与,在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。它采用短路来防止不必要的工作。

  • || 叫逻辑或,在其操作数中找到第一个真值表达式并返回它。这也使用了短路来防止不必要的工作。在支持 ES6 默认函数参数之前,它用于初始化函数中的默认参数值。

  • !! 运算符可以将右侧的值强制转换为布尔值,这也是将值转换为布尔值的一种简单方法。

3. js的数据类型的转换

在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值(调用Boolean()方法)

  • 转换为数字(调用Number()、parseInt()和parseFloat()方法)

  • 转换为字符串(调用.toString()或者String()方法)

null和underfined没有.toString方法
由浅入深,66条JavaScript面试知识点_第2张图片
此外还有一些操作符会存在隐式转换,此处不做展开,可自行百度00

4. JS中数据类型的判断( typeof,instanceof,constructor,Object.prototype.toString.call()

(1)typeof

typeof 对于原始类型来说,除了 null 都可以显示正确的类型

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object     []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object     null 的数据类型被 typeof 解释为 object

typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型,所以想判断一个对象的正确类型,这时候可以考虑使用 instanceof

(2)instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true    
// console.log(undefined instanceof Undefined);
// console.log(null instanceof Null);

可以看出直接的字面量值判断数据类型,instanceof可以精准判断引用数据类型(Array,Function,Object),而基本数据类型不能被instanceof精准判断。

我们来看一下 instanceof 在MDN中的解释:instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。其意思就是判断对象是否是某一数据类型(如Array)的实例,请重点关注一下是判断一个对象是否是数据类型的实例。在这里字面量值,2, true ,'str'不是实例,所以判断值为false。

(3)constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
这里有一个坑,如果我创建一个对象,更改它的原型,constructor就会变得不可靠了
function Fn(){};

Fn.prototype=new Array();

var f=new Fn();

console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true 

(4)Object.prototype.toString.call()
使用 Object 对象的原型方法 toString ,使用 call 进行狸猫换太子,借用Object的 toString  方法

var a = Object.prototype.toString;

console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

5. 介绍 js 有哪些内置对象?

涉及知识点:

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在
全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。

标准内置对象的分类

(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。

例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。

例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。

例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。

例如 Number、Math、Date

(5)字符串,用来表示和操作字符串的对象。

例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。

例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。

例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。

例如 JSON 等

(10)控制抽象对象

例如 Promise、Generator 等

(11)反射

例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。

例如 Intl、Intl.Collator 等

(13)WebAssembly

(14)其他

例如 arguments

js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js
定义的一些全局值属性、函数和用来实例化其他对象的构造函
数对象。一般我们经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构
造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。

详细资料可以参考:
《标准内置对象的分类》
《JS 所有内置对象属性和方法汇总》

6. undefined 与 undeclared 的区别?

已在作用域中声明但还没有赋值的变量,是 undefined 的。相反,还没有在作用域中声明过的变量,是 undeclared 的。

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

7. null 和 undefined 的区别?

首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

undefined 代表的含义是未定义,
null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null
主要用于赋值给一些可能会返回对象的变量,作为初始化。

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

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

详细资料可以参考:

《JavaScript 深入理解之 undefined 与 null》

8. {} 和 [] 的 valueOf 和 toString 的结果是什么?

{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"

[] 的 valueOf 结果为 [] ,toString 的结果为 ""

9. Javascript 的作用域和作用域链

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

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

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

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

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

详细资料可以参考:
《JavaScript 深入理解之作用域链》

也可以看看我的文章:「前端料包」深究JavaScript作用域(链)知识点和闭包

10. javascript 创建对象的几种方式?

我们一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js
和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是我们可以使用函数来进行模拟,从而产生出可复用的对象
创建方式,我了解到的方式有这么几种:

(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。

(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么我们就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此我们可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此我们可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次我们都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。

(3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此我们可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此我们可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。

(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。

嗯我目前了解到的就是这么几种方式。

详细资料可以参考:
《JavaScript 深入理解之对象创建》

11. JavaScript 继承的几种实现方式?

我了解的 js 中实现继承的几种方式有:

(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。

(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。

(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点是没有办法实现函数的复用。

(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。

详细资料可以参考:
《JavaScript 深入理解之继承》

12. 寄生式组合继承的实现?

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

Person.prototype.sayName = function() {
  console.log("My name is " + this.name + ".");
};

function Student(name, grade) {
  Person.call(this, name);
  this.grade = grade;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

Student.prototype.sayMyGrade = function() {
  console.log("My grade is " + this.grade + ".");

};

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

详情可看我之前的文章:「前端料包」一文彻底搞懂JavaScript中的this、call、apply和bind

  1. 在浏览器里,在全局范围内this 指向window对象;

  2. 在函数中,this永远指向最后调用他的那个对象;

  3. 构造函数中,this指向new出来的那个新的对象;

  4. call、apply、bind中的this被强绑定在指定的那个对象上;

  5. 箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来;

  6. apply、call、bind都是js给函数内置的一些API,调用他们可以为函数指定this的执行,同时也可以传参。

由浅入深,66条JavaScript面试知识点_第3张图片

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

在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对
象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,在这个对象的内部
将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们
是不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来让我们访问这个属性,但是我们最好不要使用这
个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对
象的原型。

当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又
会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就
是我们新建的对象为什么能够使用 toString() 等方法的原因。

特点:

JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与
之相关的对象也会继承这一改变。

参考文章:

《JavaScript 深入理解之原型与原型链》

也可以看看我写的:「前端料包」深入理解JavaScript原型和原型链

15. js 获取原型的方法?

  • p.proto

  • p.constructor.prototype

  • Object.getPrototypeOf(p)

16. 什么是闭包,为什么要用它?

闭包是指有权访问另一个函数作用域内变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以
访问到当前函数的局部变量。

闭包有两个常用的用途。

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。

  • 函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

function a(){
    var n = 0;
    function add(){
       n++;
       console.log(n);
    }
    return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1();    //1
a1();    //2  第二次调用n变量还在内存中

其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。

17. 什么是 DOM 和 BOM?

DOM  指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。

BOM  指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM
的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)
对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 locati
on 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对
象的子对象。

相关资料:

《DOM, DOCUMENT, BOM, WINDOW 有什么区别?》

《Window 对象》

《DOM 与 BOM 分别是什么,有何关联?》

《JavaScript 学习总结(三)BOM 和 DOM 详解》

18. 三种事件模型是什么?

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

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

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

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

相关资料:

《一个 DOM 元素绑定多个事件时,先执行冒泡还是捕获》

19. 事件委托是什么?

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

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

相关资料:

《JavaScript 事件委托详解》

20. 什么是事件传播?

事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在“当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在“冒泡阶段”中,事件冒泡或向上传播至父级,祖父母,祖父母或父级,直到到达window为止;而在“捕获阶段”中,事件从window开始向下触发元素 事件或event.target。

事件传播有三个阶段:”中,事件冒泡或向上传播至父级,祖父母,祖父母或父级,直到到达window为止;而在“捕获阶段”中,事件从window开始向下触发元素 事件或event.target

事件传播有三个阶段:

  1. 捕获阶段–事件从 window 开始,然后向下到每个元素,直到到达目标元素。

  2. 目标阶段–事件已达到目标元素。

  3. 冒泡阶段–事件从目标元素冒泡,然后上升到每个元素,直到到达 window。

21. 什么是事件捕获?

当事件发生在 DOM 元素上时,该事件并不完全发生在那个元素上。在捕获阶段,事件从window开始,一直到触发事件的元素。window----> document----> html----> body ---->目标元素

假设有如下的 HTML 结构:


  
    1
  

对应的 JS 代码:

function addEvent(el, event, callback, isCapture = false) {
  if (!el || !event || !callback || typeof callback !== 'function') return;
  if (typeof el === 'string') {
    el = document.querySelector(el);
  };
  el.addEventListener(event, callback, isCapture);
}

addEvent(document, 'DOMContentLoaded', () => {
  const child = document.querySelector('.child');
  const parent = document.querySelector('.parent');
  const grandparent = document.querySelector('.grandparent');

  addEvent(child, 'click', function (e) {
    console.log('child');
  });

  addEvent(parent, 'click', function (e) {
    console.log('parent');
  });

  addEvent(grandparent, 'click', function (e) {
    console.log('grandparent');
  });

  addEvent(document, 'click', function (e) {
    console.log('document');
  });

  addEvent('html', 'click', function (e) {
    console.log('html');
  })

  addEvent(window, 'click', function (e) {
    console.log('window');
  })

});

addEventListener方法具有第三个可选参数useCapture,其默认值为false,事件将在冒泡阶段中发生,如果为true,则事件将在捕获阶段中发生。如果单击child元素,它将分别在控制台上打印windowdocumenthtmlgrandparentparent,这就是事件捕获

22. 什么是事件冒泡?

事件冒泡刚好与事件捕获相反,当前元素---->body ----> html---->document ---->window。当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在冒泡阶段,事件冒泡,或者事件发生在它的父代,祖父母,祖父母的父代,直到到达window为止。

假设有如下的 HTML 结构:


  
    1
  

对应的JS代码:

function addEvent(el, event, callback, isCapture = false) {
  if (!el || !event || !callback || typeof callback !== 'function') return;
  if (typeof el === 'string') {
    el = document.querySelector(el);
  };
  el.addEventListener(event, callback, isCapture);
}

addEvent(document, 'DOMContentLoaded', () => {
  const child = document.querySelector('.child');
  const parent = document.querySelector('.parent');
  const grandparent = document.querySelector('.grandparent');

  addEvent(child, 'click', function (e) {
    console.log('child');
  });

  addEvent(parent, 'click', function (e) {
    console.log('parent');
  });

  addEvent(grandparent, 'click', function (e) {
    console.log('grandparent');
  });

  addEvent(document, 'click', function (e) {
    console.log('document');
  });

  addEvent('html', 'click', function (e) {
    console.log('html');
  })

  addEvent(window, 'click', function (e) {
    console.log('window');
  })

});

addEventListener方法具有第三个可选参数useCapture,其默认值为false,事件将在冒泡阶段中发生,如果为true,则事件将在捕获阶段中发生。如果单击child元素,它将分别在控制台上打印childparentgrandparenthtmldocumentwindow,这就是事件冒泡

23. DOM 操作——怎样添加、移除、移动、复制、创建和查找节点?

(1)创建新节点

  createDocumentFragment()    //创建一个DOM片段
  createElement()   //创建一个具体的元素
  createTextNode()   //创建一个文本节点

(2)添加、移除、替换、插入

appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)

(3)查找

getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();

(4)属性操作

getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);

相关资料:

《DOM 概述》

《原生 JavaScript 的 DOM 操作汇总》

《原生 JS 中 DOM 节点相关 API 合集》

24. js数组和对象有哪些原生方法,列举一下

由浅入深,66条JavaScript面试知识点_第4张图片 由浅入深,66条JavaScript面试知识点_第5张图片

25. 常用的正则表达式

//(1)匹配 16 进制颜色值
var color = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;

//(2)匹配日期,如 yyyy-mm-dd 格式
var date = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

//(3)匹配 qq 号
var qq = /^[1-9][0-9]{4,10}$/g;

//(4)手机号码正则
var phone = /^1[34578]\d{9}$/g;

//(5)用户名正则
var username = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;

//(6)Email正则
var email = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;

//(7)身份证号(18位)正则
var cP = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;

//(8)URL正则
var urlP= /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;

// (9)ipv4地址正则
var ipP = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

// (10)//车牌号正则
var cPattern = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/;

// (11)强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):var pwd = /^(?=.\d)(?=.[a-z])(?=.[A-Z]).{8,10}$/

26. Ajax 是什么? 如何创建一个 Ajax?

我对 ajax 的理解是,它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。

创建步骤:

由浅入深,66条JavaScript面试知识点_第6张图片

面试手写(原生):

//1:创建Ajax对象
var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本
//2:配置 Ajax请求地址
xhr.open('get','index.xml',true);
//3:发送请求
xhr.send(null); // 严谨写法
//4:监听请求,接受响应
xhr.onreadysatechange=function(){
     if(xhr.readySates==4&&xhr.status==200 || xhr.status==304 )
          console.log(xhr.responsetXML)
}

jQuery写法

  $.ajax({
          type:'post',
          url:'',
          async:ture,//async 异步  sync  同步
          data:data,//针对post请求
          dataType:'jsonp',
          success:function (msg) {

          },
          error:function (error) {

          }
        })

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;
}

27. js 延迟加载的方式有哪些?

js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。

我了解到的几种方式是:

  1. 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

  2. 给 js 脚本添加 defer属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。

  3. 给 js 脚本添加 async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。

  4. 动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。

相关资料:

《JS 延迟加载的几种方式》

《HTML 5

  • ES6 的 proxy 阮一峰Proxy

  • jQuery.proxy()方法

优点

  • 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用

  • 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;

缺点

  • 处理请求速度可能有差别,非直接访问存在开销

3. 工厂模式(Factory Pattern)

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。

class Product {
    constructor(name) {
        this.name = name
    }
    init() {
        console.log('init')
    }
    fun() {
        console.log('fun')
    }
}

class Factory {
    create(name) {
        return new Product(name)
    }
}

// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()

场景

  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择

  • 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;

  • 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性

优点

  • 创建对象的过程可能很复杂,但我们只需要关心创建结果。

  • 构造函数和创建者分离, 符合“开闭原则”

  • 一个调用者想创建一个对象,只要知道其名称就可以了。

  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

缺点

  • 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度

  • 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度

什么时候不用

  • 当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。

  • 由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。

4. 单例模式(Singleton Pattern)

顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。

实现单例模式需要解决以下几个问题:

  • 如何确定Class只有一个实例?

  • 如何简便的访问Class的唯一实例?

  • Class如何控制实例化的过程?

  • 如何将Class的实例个数限制为1?

我们一般通过实现以下两点来解决上述问题:

  • 隐藏Class的构造函数,避免多次实例化

  • 通过暴露一个 getInstance() 方法来创建/获取唯一实例

Javascript中单例模式可以通过以下方式实现:

// 单例构造器
const FooServiceSingleton = (function () {
  // 隐藏的Class的构造函数
  function FooService() {}

  // 未初始化的单例对象
  let fooService;

  return {
    // 创建/获取单例对象的函数
    getInstance: function () {
      if (!fooService) {
        fooService = new FooService();
      }
      return fooService;
    }
  }
})();

实现的关键点有:

  1. 使用 IIFE创建局部作用域并即时执行;

  2. getInstance()为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回。

我们可以验证下单例对象是否创建成功:

const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();

console.log(fooService1 === fooService2); // true

场景例子

  • 定义命名空间和实现分支型方法

  • 登录框

  • vuex 和 redux中的store

优点

  • 划分命名空间,减少全局变量

  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护

  • 且只会实例化一次。简化了代码的调试和维护

缺点

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合

  • 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一

  • 个单元一起测试。

5. 策略模式(Strategy Pattern)

策略模式简单描述就是:对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们可以互相替换



    策略模式-校验表单
    


    
        用户名:
        密码:
        手机号码:
        提交
    
    
        // 策略对象
        const strategies = {
          isNoEmpty: function (value, errorMsg) {
            if (value === '') {
              return errorMsg;
            }
          },
          isNoSpace: function (value, errorMsg) {
            if (value.trim() === '') {
              return errorMsg;
            }
          },
          minLength: function (value, length, errorMsg) {
            if (value.trim().length < length) {
              return errorMsg;
            }
          },
          maxLength: function (value, length, errorMsg) {
            if (value.length > length) {
              return errorMsg;
            }
          },
          isMobile: function (value, errorMsg) {
            if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
              return errorMsg;
            }                
          }
        }

        // 验证类
        class Validator {
          constructor() {
            this.cache = []
          }
          add(dom, rules) {
            for(let i = 0, rule; rule = rules[i++];) {
              let strategyAry = rule.strategy.split(':')
              let errorMsg = rule.errorMsg
              this.cache.push(() => {
                let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg)
                return strategies[strategy].apply(dom, strategyAry)
              })
            }
          }
          start() {
            for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
              let errorMsg = validatorFunc()
              if (errorMsg) {
                return errorMsg
              }
            }
          }
        }

        // 调用代码
        let registerForm = document.getElementById('registerForm')

        let validataFunc = function() {
          let validator = new Validator()
          validator.add(registerForm.userName, [{
            strategy: 'isNoEmpty',
            errorMsg: '用户名不可为空'
          }, {
            strategy: 'isNoSpace',
            errorMsg: '不允许以空白字符命名'
          }, {
            strategy: 'minLength:2',
            errorMsg: '用户名长度不能小于2位'
          }])
          validator.add(registerForm.password, [ {
            strategy: 'minLength:6',
            errorMsg: '密码长度不能小于6位'
          }])
          validator.add(registerForm.phoneNumber, [{
            strategy: 'isMobile',
            errorMsg: '请输入正确的手机号码格式'
          }])
          return validator.start()
        }

        registerForm.onsubmit = function() {
          let errorMsg = validataFunc()
          if (errorMsg) {
            alert(errorMsg)
            return false
          }
        }
    


场景例子

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的'行为',那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。

  • 一个系统需要动态地在几种算法中选择一种。

  • 表单验证

优点

  • 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句

  • 提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展

  • 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案

缺点

  • 会在程序中增加许多策略类或者策略对象

  • 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy

6. 迭代器模式(Iterator Pattern)

如果你看到这,ES6中的迭代器 Iterator 相信你还是有点印象的,上面第60条已经做过简单的介绍。迭代器模式简单的说就是提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。

迭代器模式解决了以下问题:

  • 提供一致的遍历各种数据结构的方式,而不用了解数据的内部结构

  • 提供遍历容器(集合)的能力而无需改变容器的接口

一个迭代器通常需要实现以下接口:

  • hasNext():判断迭代是否结束,返回Boolean

  • next():查找并返回下一个元素

为Javascript的数组实现一个迭代器可以这么写:

const item = [1, 'red', false, 3.14];

function Iterator(items) {
  this.items = items;
  this.index = 0;
}

Iterator.prototype = {
  hasNext: function () {
    return this.index < this.items.length;
  },
  next: function () {
    return this.items[this.index++];
  }
}

验证一下迭代器是否工作:

const iterator = new Iterator(item);

while(iterator.hasNext()){
  console.log(iterator.next());
}
//输出:1, red, false, 3.14

ES6提供了更简单的迭代循环语法 for…of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator 的方法,该方法返回一个iterator对象。

比如我们实现一个 Range 类用于在某个数字区间进行迭代:

function Range(start, end) {
  return {
    [Symbol.iterator]: function () {
      return {
        next() {
          if (start < end) {
            return { value: start++, done: false };
          }
          return { done: true, value: end };
        }
      }
    }
  }
}

验证一下:

for (num of Range(1, 5)) {
  console.log(num);
}
// 输出:1, 2, 3, 4

7. 观察者模式(Observer Pattern)

观察者模式又称发布-订阅模式(Publish/Subscribe Pattern),是我们经常接触到的设计模式,日常生活中的应用也比比皆是,比如你订阅了某个博主的频道,当有内容更新时会收到推送;又比如JavaScript中的事件订阅响应机制。观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者

观察者模式中Subject对象一般需要实现以下API:

  • subscribe(): 接收一个观察者observer对象,使其订阅自己

  • unsubscribe(): 接收一个观察者observer对象,使其取消订阅自己

  • fire(): 触发事件,通知到所有观察者

用JavaScript手动实现观察者模式:

// 被观察者
function Subject() {
  this.observers = [];
}

Subject.prototype = {
  // 订阅
  subscribe: function (observer) {
    this.observers.push(observer);
  },
  // 取消订阅
  unsubscribe: function (observerToRemove) {
    this.observers = this.observers.filter(observer => {
      return observer !== observerToRemove;
    })
  },
  // 事件触发
  fire: function () {
    this.observers.forEach(observer => {
      observer.call();
    });
  }
}

验证一下订阅是否成功:

const subject = new Subject();

function observer1() {
  console.log('Observer 1 Firing!');
}


function observer2() {
  console.log('Observer 2 Firing!');
}

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire();

//输出:
Observer 1 Firing! 
Observer 2 Firing!

验证一下取消订阅是否成功:

subject.unsubscribe(observer2);
subject.fire();

//输出:
Observer 1 Firing!

场景

  • DOM事件

document.body.addEventListener('click', function() {
    console.log('hello world!');
});
document.body.click()
  • vue 响应式

优点

  • 支持简单的广播通信,自动通知所有已经订阅过的对象

  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用

  • 增加了灵活性

  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。

缺点

  • 过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

8. 中介者模式(Mediator Pattern)

在中介者模式中,中介者(Mediator)包装了一系列对象相互作用的方式,使得这些对象不必直接相互作用,而是由中介者协调它们之间的交互,从而使它们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用,保证这些作用可以彼此独立的变化。

中介者模式和观察者模式有一定的相似性,都是一对多的关系,也都是集中式通信,不同的是中介者模式是处理同级对象之间的交互,而观察者模式是处理Observer和Subject之间的交互。中介者模式有些像婚恋中介,相亲对象刚开始并不能直接交流,而是要通过中介去筛选匹配再决定谁和谁见面。

场景

  • 例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。

var goods = {   //手机库存
    'red|32G': 3,
    'red|64G': 1,
    'blue|32G': 7,
    'blue|32G': 6,
};
//中介者
var mediator = (function() {
    var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numSelect = document.getElementById('numSelect');
    return {
        changed: function(obj) {
            switch(obj){
                case colorSelect:
                    //TODO
                    break;
                case memorySelect:
                    //TODO
                    break;
                case numSelect:
                    //TODO
                    break;
            }
        }
    }
})();
colorSelect.onchange = function() {
    mediator.changed(this);
};
memorySelect.onchange = function() {
    mediator.changed(this);
};
numSelect.onchange = function() {
    mediator.changed(this);
};
  • 聊天室里

聊天室成员类:

function Member(name) {
  this.name = name;
  this.chatroom = null;
}

Member.prototype = {
  // 发送消息
  send: function (message, toMember) {
    this.chatroom.send(message, this, toMember);
  },
  // 接收消息
  receive: function (message, fromMember) {
    console.log(`${fromMember.name} to ${this.name}: ${message}`);
  }
}

聊天室类:

function Chatroom() {
  this.members = {};
}

Chatroom.prototype = {
  // 增加成员
  addMember: function (member) {
    this.members[member.name] = member;
    member.chatroom = this;
  },
  // 发送消息
  send: function (message, fromMember, toMember) {
    toMember.receive(message, fromMember);
  }
}

测试一下:

const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');

chatroom.addMember(bruce);
chatroom.addMember(frank);

bruce.send('Hey frank', frank);

//输出:bruce to frank: hello frank

优点

  • 使各对象之间耦合松散,而且可以独立地改变它们之间的交互

  • 中介者和对象一对多的关系取代了对象之间的网状多对多的关系

  • 如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码

缺点

  • 系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。

9. 访问者模式(Visitor Pattern)

访问者模式 是一种将算法与对象结构分离的设计模式,通俗点讲就是:访问者模式让我们能够在不改变一个对象结构的前提下能够给该对象增加新的逻辑,新增的逻辑保存在一个独立的访问者对象中。访问者模式常用于拓展一些第三方的库和工具。

// 访问者  
class Visitor {
    constructor() {}
    visitConcreteElement(ConcreteElement) {
        ConcreteElement.operation()
    }
}
// 元素类  
class ConcreteElement{
    constructor() {
    }
    operation() {
       console.log("ConcreteElement.operation invoked");  
    }
    accept(visitor) {
        visitor.visitConcreteElement(this)
    }
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)

访问者模式的实现有以下几个要素:

  • Visitor Object:访问者对象,拥有一个visit()方法

  • Receiving Object:接收对象,拥有一个accept() 方法

  • visit(receivingObj):用于Visitor接收一个Receiving Object

  • accept(visitor):用于Receving Object接收一个Visitor,并通过调用Visitorvisit() 为其提供获取Receiving Object数据的能力

简单的代码实现如下:

Receiving Object:

function Employee(name, salary) {
  this.name = name;
  this.salary = salary;
}

Employee.prototype = {
  getSalary: function () {
    return this.salary;
  },
  setSalary: function (salary) {
    this.salary = salary;
  },
  accept: function (visitor) {
    visitor.visit(this);
  }
}
Visitor Object:

function Visitor() { }

Visitor.prototype = {
  visit: function (employee) {
    employee.setSalary(employee.getSalary() * 2);
  }
}

验证一下:

const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);

console.log(employee.getSalary());//输出:2000

场景

  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作

  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。

优点

  • 符合单一职责原则

  • 优秀的扩展性

  • 灵活性

缺点

  • 具体元素对访问者公布细节,违反了迪米特原则

  • 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。

  • 具体元素变更比较困难

相关参考资料:

JavaScript设计模式es6(23种)

《前端面试之道》

《JavaScript 设计模式》

《JavaScript 中常见设计模式整理》

后话

以上66条便是这几天复盘的总结,整体上是按照由浅入深的顺序来的,小部分内容并非原创,相关的参考我都有在每条的末尾贴了链接,在这里要特别感谢各路大佬的博客,给了我很多帮助~

前端是个大杂烩,各种框架层出不穷,但万变不离JS,务实基础才是根本,如果你觉得本文对你有所帮助,点个star支持一下吧 https://github.com/JakeZhangZJK/myBlog

学习交流

  • 关注公众号【前端宇宙】,每日获取好文推荐

  • 添加微信,入群交流


“在看和转发”就是最大的支持

你可能感兴趣的:(由浅入深,66条JavaScript面试知识点)