由浅入深 65个JS常考面试题

由浅入深逐个击破 JS常考面试题(上篇)

1、 介绍一下JS的基本数据类型,值是如何存储的?

JavaScript一共有8种数据类型,其中有7种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示独一无二的值)和BigInt(es10新增);
一种引用类型Object(本质上是由一组无序的名值对组成的),里面包含function、Array、Date,JS不支持任何自定义类型的机制,所有的值都是上述的八种数据类型之一。
如何存储???
基本数据类型:直接存储在栈中,占据空间小,大小固定,属于被频繁使用的数据,所以放入栈中存储
引用数据类型:同时存储在堆和栈中,占据空间大,大小不固定,引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址之后从堆中获得实体

2、&&、||、!!运算符能做什么

&&称为逻辑与,在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。采用短路防止不必要的工作
||称为逻辑或,在其操作数中找到第一个真值表达式并返回它,采用短路防止不必要的工作
!!可以将右侧的值强制转换成布尔值,这也是将值转换成布尔值的一种简单方法

3、JS的数据类型的转换

  1. 转换为布尔值(调用Boolean()方法)
  2. 转换为数字(调用Number()、parseInt()、parseFloat())
  3. 转换为字符串(调用toString()或者String()方法)
    null和undefinede没有toString方法
    由浅入深 65个JS常考面试题_第1张图片

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

**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

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精准判断。
MDN中的解释:instanceof运算符用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。其意思就是判断对象是否是某一数据类型的实例,在这里字面量值2、true、‘str’不是实例,所以判断就是false
constructor:

console.log([].constructor == Array, {}.constructor == Object)
//true true

Object.prototype.toString.call()
用于精准的判断数据类型,借助于Object对象的原型方法toString,使用call来进行狸猫换太子

var getDataType=Object.prototype.toString;
console.log(getDataType.call('字符串'), getDataType.call(1), getDataType.call(true), getDataType.call(undefined),getDataType.call(null), getDataType.call({}), getDataType.call([]), getDataType.call(function(){}));
//[object String] [object Number] [object Boolean] [object Undefined] [object Null] [object Object] [object Array] [object Function]

5、undefined和undeclared的区别

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

6. null 和 undefined 的区别?

undefined和null都是基本数据类型,这两个基本数据类型分别都只有一个值,就是undefined和null
undefined代表未定义,null代表的含义是空对象,一般变量声明了但是还没有定义返回undefined,null主要是用于赋值给一些可能会返回对象的变量作为初始化
当我们使用typeof判断数据类型的时候,null会返回Object,这是一个历史遗留问题,当我们使用双等号对两种类型的值进行比较的时候返回true,三等号的时候返回false

7、{}和[]的valueOf和toString的结果是什么?

toString会将对象转换成字符串,valueOf是把对象转换成基本数据类型的值
{}的valueOf结果是{},toString()的结果为"[Object Object]"
[]的valueOf结果是[],toString()的结果是""

8、JS的作用域和作用域链

作用域:
作用域就是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套作用域根据变量标识符进行变量查找
作用域链:
作用域链的作用就是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数
作用域链的本质上是一个指向变量对象的指针列表,变量对象是一个包含了执行环境中所有变量和函数的对象,作用域的前端始终都是当前执行上下文的变量对象,全局执行上下文的变量对象也就是全局对象始终是作用域链的最后一个对象,当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链一直向后找

9、JS创建对象的6种方式

(1)new操作符+Object创建对象

var person = new Object();
    person.name = "lisi";
    person.age = 21;
    person.family = ["lida","lier","wangwu"];
    person.say = function(){
        alert(this.name);
    }

(2)字面量创建对象

var person ={
        name: "lisi",
        age: 21,
        family: ["lida","lier","wangwu"],
        say: function(){
            alert(this.name);
        }
    };

由于上面两种创建方式使用同一接口创建了大量相似的对象,产生了大量的重复代码,因此产生了工厂模式
(3)工厂模式
工作原理就是用函数来封装创建对象的细节,从而调用函数来达到复用的目的,它只是简单的封装了复用代码,而没有建立起对象和类间的关系,没有解决对象识别的问题(但是工厂模式却无从识别对象的类型,因为全部都是Object,不像Date、Array等,本例中,得到的都是o对象,对象的类型都是Object,因此出现了构造函数模式)。

function createPerson(name,age,family) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.family = family;
    o.say = function(){
        alert(this.name);
    }
    return o;
}

var person1 =  createPerson("lisi",21,["lida","lier","wangwu"]);   //instanceof无法判断它是谁的实例,只能判断他是对象,构造函数都可以判断出
var person2 =  createPerson("wangwu",18,["lida","lier","lisi"]);
console.log(person1 instanceof Object);                           //true

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

function Person(name,age,family) {
    this.name = name;
    this.age = age;
    this.family = family;
    this.say = function(){
        alert(this.name);
    }
}
var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
var person2 = new Person("lisi",21,["lida","lier","lisi"]);
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true
console.log(person1.constructor);      //constructor 属性返回对创建此对象的数组、函数的引用

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

function Person() {
}

Person.prototype.name = "lisi";
Person.prototype.age = 21;
Person.prototype.family = ["lida","lier","wangwu"];
Person.prototype.say = function(){
    alert(this.name);
};
console.log(Person.prototype);   //Object{name: 'lisi', age: 21, family: Array[3]}

var person1 = new Person();        //创建一个实例person1
console.log(person1.name);        //lisi

var person2 = new Person();        //创建实例person2
person2.name = "wangwu";
person2.family = ["lida","lier","lisi"];
console.log(person2);            //Person {name: "wangwu", family: Array[3]}
// console.log(person2.prototype.name);         //报错
console.log(person2.age);              //21

(6)混合模式(构造函数+原型模式)

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

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

Person.prototype = {
    constructor: Person,  //每个函数都有prototype属性,指向该函数原型对象,原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针
    say: function(){
        alert(this.name);
    }
}

var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
console.log(person1);
var person2 = new Person("wangwu",21,["lida","lier","lisi"]);
console.log(person2);

11、JS继承的方式

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

function Parent(name,gender){
            this.name=name;
            this.gender=gender;
            this.list=[1,2,3]
        }
        Parent.prototype.eat=function(){
            console.log("晚餐时间到")
        }
        function Child(age){
            this.age=age;
        }
        Child.prototype=new Parent("李白","男");
        var child=new Child(20);
        var child2=new Child(30);
        child.eat();
        console.log(child.list,child2.list);// [1,2,3] [1,2,3]
        child.list.push(4)
        console.log(child.list);// [1,2,3,4]        
        console.log(child2.list);// [1,2,3,4]

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

function Parent(name){
    this.name=name;
    }
    Parent.prototype.saiHi=function(){
        console.log("hello")
    }
    function Child(name,age,gender){
        Parent.call(this,name)
        this.age=age;
        this.gender=gender;
    }
    let child=new Child("王磊",20,"男")
    console.log(child.name);// 王磊
    child.sayHi(); // Uncaught TypeError:child.sayHi is not a function

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

function Person(school){
            this.school=school;
        }
        Person.prototype.skill=function(){
            console.log("学习");
        }
        function Student(school,name,age,gender){
            Parent.call(this,school);
            this.name=name;
            this.age=age;
            this.gender=gender;
        }
        Student.prototype=Person.prototype;
        let student=new Student("广铁一中","王菲菲",14,"女");
        console.log(Student.prototype===Person.prototype)
        console.log(student.constructor)

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

function Parent(name,play){
            this.name=name;
            this.play=play;
        }
        function Child(name,play,age){
            Parent.call(this,name,play);
            this.age=age;
        }
        Child.prototype=Object.create(Parent.prototype);// 隔离了父类和子类的构造函数,父类的添加到了__proto__属性上
        Child.prototype.constructor=Child
        let child=new Child("张三","玩",20);
        let child2=new Child("李四","吃",10)
       
        console.log(child.constructor)

(5)寄生式继承

12、谈谈你对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的执行,同时也可以传参
    由浅入深 65个JS常考面试题_第2张图片

13、JS原型、原型链有什么特点?

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

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

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

14、JS获取原型的方法?

p.proto
p.constructor.prototype
Object.getPrototypeOf§

15、什么是闭包?为什么要使用它?

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

  1. 闭包的第一个用途就是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量
  2. 闭包的另一个用途就是使已经运行结束的上下文中的变量继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收
function a(){
    var n = 0;
    function add(){
       n++;
       console.log(n);
    }
    return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1();    //1
a1();    //2  第二次调用n变量还在内存中

16、什么是DOM和BOM?

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

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

17、三种事件模型是什么?

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

  1. DOM0级模型:这种模型不会传播,事件一旦发生将马上进行处理所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过js属性来指定监听函数,相同事件的监听函数只能绑定一个,这种方式是所有浏览器所兼容的。
  2. IE事件模型:在该事件模型中,一次事件一共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行,这种模型通过attachEvent来添加监听函数,可以添加多个监听函数会按顺序执行
  3. DOM2级事件模型:在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段,指的是事件从document一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段的和IE事件模型的两个阶段相同。这种事件模型,事件绑定的函数是addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。

18、什么是事件委托?

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

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

19、什么是事件传播?

当事件发生在DOM元素上时,该事件并不完全发生在那个元素上,在“当事件发生在DOM元素上时,该事件并不完全发生在那个元素上”,在冒泡阶段中,事件冒泡或向上传播至父级,祖父母,直到到达window为止,而在捕获阶段,事件从window开始向下触发元素事件或者event.target
事件传播分成三个阶段:
1、捕获阶段–事件从 window 开始,然后向下到每个元素,直到到达目标元素。
2、目标阶段–事件已达到目标元素。
3、冒泡阶段–事件从目标元素冒泡,然后上升到每个元素,直到到达 window。

20、什么是事件捕获?

当事件发生在DOM元素上的时候,该事件并不完全发生在那个元素上,在捕获阶段,事件从window开始,一直到触发事件的元素,window----->document—>html—>body—>目标元素
addEventListener方法具有第三个可选参数usecapture,其默认值是false,事件将在冒泡阶段中发生,如果为true,则事件将在捕获阶段中发生,如果单击child元素,它将分别在控制台上打印window,document,html,grandparent和parent,这就是事件捕获。
由浅入深 65个JS常考面试题_第3张图片

21、什么是事件冒泡?

事件冒泡刚好与事件捕获相反,当前元素—>body—>html—>document---->window,当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。
addEventListener方法具有第三个可选参数useCapture,其默认值为false,事件将在冒泡阶段中发生,如果为true,则事件将在捕获阶段中发生。如果单击child元素,它将分别在控制台上打印child,parent,grandparent,html,document和window,这就是事件冒泡。

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

  1. 创建新节点

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

  1. 添加、移除、替换、插入

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

  1. 查找

getElementById()
getElementByname()
getElementByClassName()
querySelector()
querySelectorAll()

  1. 属性操作

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

23、js数组和对象有哪些原生的方法

由浅入深 65个JS常考面试题_第4张图片
由浅入深 65个JS常考面试题_第5张图片

24、常用正则表达式

//(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}$/

25、Ajax是什么?如何创建一个Ajax?

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

  • 创建XMLHttpRequest对象
  • 配置Ajax请求地址
  • 发送请求
  • 监听请求,接受响应
    面试要求手写(原生):
//1、创建Ajax对象
var xhr=window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');//兼容IE6及以下的版本
//2、配置Ajax请求地址
xhr.open('get','index.html',true);
//3、发送请求
xhr.send(null);//严谨写法
//4、监听请求,接受响应
xhr.onreadystatechange=fucntion(){
//xhr.readyState==4表示响应内容解析完成可以在客户端调用
	if(xhr.readyState==4&&xhr.status==200||xhr.status==304)
	console.log(xhr.responseXML)
}

jquery写法:

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

promise封装实现:

function getJSON(url)
{	
		//创建一个promise对象
		let promise=new Promise(function(resolve,reject){
	//创建一个XMLHttpRequest对象
	let xhr=new XMLHttpRequest();
	//配置一个请求
	第三个参数就是不要等到有请求返回的时候接着往下执行,这就是所谓的异步获取数据
	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.setresponseHeader("Accept","application/jsonp");
	//发送请求
	xhr.send(null);
});
	return promise;
}

补充状态码:
1xx:信息性状态码,表示接受请求正在处理
2xx:成功状态码,表示请求正常处理完毕
3xx:重定向状态码,表示需要进行附加操作以完成请求
4xx:客户端错误状态,服务器无法处理请求
5xx:服务器错误状态码,服务器处理请求错误

26、JS延迟加载的方式有哪些?

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

方式:

  1. 将js的脚本放在文档的底部,来使js脚本尽可能的在最后加载执行。
  2. 给js脚本添加defer属性,这个属性会让脚本的加载与文档的解析同步解析,然后再文档解析完成之后再执行这个脚本文件,这样的话会使页面的渲染不被阻塞,多个设置了defer属性的脚本按规范来说使顺序执行的,但是再一些浏览器中可能不是这样。
  3. 给js添加async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行js脚本,这个时候如果文档还没有解析完成的话同样会阻塞。多个async属性的脚本执行顺序是不可预测的,一般不会按照代码顺序依次执行。
  4. 动态创建DOM标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态创建script标签来引入js脚本

27、谈谈你对模块化开发的理解?

一个模块就是实现一个特定功能的一组方法,再最开始的时候,js只是实现了一些简单的功能,所以并没有模块化的概念,但是随着程序越来越复杂,代码的模块化开发变得越来越重要。

由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是看这种方式容易造成全局变量的冲突,并且模块之间没有联系。

后面就提出了对象的写法,通过将函数作为一个对象的方法来实现,这样就解决了直接使用函数作为模块的一些缺点,但是这种方法会暴露所有的模块成员,外部代码可以修改内部的属性的值。

现在最常用的是立即执行函数的写法,立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。

28、JS的几种模块规范?

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

29、AMD和CMD规范的区别?

它们之间的区别有两个方面。

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

30、ES6模块与CommonJS模块、AMD、CMD的差异

  1. CommonJS输出的是一个值的拷贝,ES6模块输出的是值的引用,也就是说,CommonJS一旦输出一个值,模块内部的变化影响不到这个值,ES6模块的运行机制与CommonJS不一样,JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用,等到脚本真正执行的时候,再根据这个只读引用,到被加载的那个模块里面取值。
  2. CommonJS模块是运行时加载,ES6是编译时候输出接口,CommonJS模块就是对象,即再输入时先加载整个模块,生成一个对象,然后 再从这个对象上读取方法,这种加载称为运行时加载,而ES6不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

31、requireJS的核心原理是什么?

require.js的核心原理是通过动态常见script脚本异步引入模块,然后对每个脚本的load事件进行监听,如果每个脚本都加载完成了,再调用回调函数。

32、谈谈JS的运行机制?

1、JS单线程
JS语言的一大特点就是单线程,就是同一时间只能作一件事情。

JS的单线程与它的用途有关,作为浏览器脚本语言,JS的主要用途就是与用户互动以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JS同时有两个线程,一个线程再某个DOM节点上添加内容,另外一个线程就是删除这个节点,这时浏览器以哪个线程为主呢??
所以为了避免复杂性,从一开始诞生,JS就是单线程,这已经称为这门语言的核心特征。

2、JS事件循环
JS代码执行过程种会有很多任务,这些任务分为两类:
同步任务和异步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的的渲染,而像加载图片音乐之类占用资源大耗时久的任务就是异步任务
由浅入深 65个JS常考面试题_第6张图片
我们来解释一下这张图:

  • 同步和异步任务分别进入不同的执行场所,同步的进入主线程,异步的进入Event Table并注册回调函数
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue(任务队列)
  • 主线程内的任务执行完毕为空,会去Event Queue里面读取对应的函数进入主线程。
  • 上述过程会不断地重复,也就是常说的Event Loop(事件循环)
    那主线程执行栈何时为空呢?
    JS引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue里面检查是否有等待被调用的函数。
    以上就是JS运行的整体流程
    需要注意的是除了同步任务和异步任务,任务还可以更加细分为宏任务和微任务,JS引擎会优先执行微任务。
    微任务包括了promise的回调、node中的process.nextTick、对Dom变化监听的MutationObserver
    宏任务包括了script脚本的执行、setTimeout、setInterval这一类的定时事件、IO操作、UI渲染

面试中如何回答???

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

最后用一道题目来检测:

setTimeout(function() {
  console.log(1)
}, 0);
new Promise(function(resolve, reject) {
  console.log(2);
  resolve()
}).then(function() {
  console.log(3)
});
process.nextTick(function () {
  console.log(4)
})
console.log(5)

第一轮:主线程开始执行,遇到setTimeout,将setTimeout回调函数丢到宏任务队列里面,再往下执行new Promise立即执行输出2,then回调函数加入到微任务队列里面,再继续执行,遇到process.nextTick,同样将回调函数放到任务队列当中,再继续向下执行,输出5,当所有的同步任务执行完成之后看有没有可以执行的微任务。返现有then函数和process.nextTick两个微任务,那么先执行哪一个呢?process.nextTick指定的异步任务总是会再所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束
第二轮:从宏任务队列开始,发现有setTimeout回调函数,输出1执行完毕
最后结果是25431

33、arguments的对象是什么?

arguments对象是函数中传递的参数值的集合,它是一个类似数组的对象,因为它有一个length属性,我们可以使用数组的索引表示法arguments[1]来访问单个值,但是它没有数组的内置方法,如forEach、reduce、filter和map
我们可以使用Arrar.prototype.slice.call(arguments)将arguments对象转换成一个数组。

function one() {
  return Array.prototype.slice.call(arguments);
}

注意:箭头函数中没有arguments对象。

function one() {
  return arguments;
}
const two = function () {
  return arguments;
}
const three = function three() {
  return arguments;
}

const four = () => arguments;

four(); // Throws an error  - arguments is not defined

当我们调用函数four时,它会抛出一个ReferenceError: arguments is not defined error。使用rest语法,可以解决这个问题。

const four = (...args) => args;

这会自动将所有参数值放入数组当中。

34、为什么再调用函数的时候,代码中的b会变成全局变量?

function myFunc() {
  let a = b = 0;
}

myFunc();

原因是赋值运算符是从右到左求值的,这意味着当多个赋值运算符出现再一个表达式中时,它们是从右向左求值得,所以上面得代码变成了这样

function myFunc() {
  let a = (b = 0);
}

myFunc();

首先,表达式b=0求值,在本例当中没有声明,因此,JS引擎在这个函数外创建了一个全局变量b,之后表达式b=0得返回值为0,并赋值给新的局部变量a。
我们可以通过在赋值之前先声明变量来解决这个问题。

function myFunc() {
  let a,b;
  a = b = 0;
}
myFunc();

35、简单介绍一下V8引擎的垃圾回收机制

新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。

新生代被分为From和To两个空间,To一般是闲置的。当From空间满了的时候会执行Scavenge算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束之后再继续执行。这个算法分成三步:
(1)首先检查From空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代,如果不满足条件就移动到To空间

(2)如果对象不存活,则释放对象的空间

(3)最后将From空间和To空间角色进行交换

新生代晋升到老生代有两个条件:
(1)第一个是判断对象是否已经经过一次Scavenge回收,若经历过,则将对象从From空间复制到老生代中,若没有经理过就复制到To空间
(2)第二个是To空间内存使用占比是否超过限制,当对象从From空间复制到To空间时,若To空间使用超过25%,则对象直接晋升到老生代,设置25%的原因就是算法结束之后会交换位置,如果To空间内存太小的话,会影响到后续内存的分配

老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束之后会清除掉那些没有标记的对象。由于标记清除之后会啊总成很多的内存碎片,不便于后面的内存分配,所以了解内存碎片的问题引入了标记压缩法。

由于再进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响,为了解决这个问题V8引入了增量标记的放方法,将一次停顿进行的过程分成了很多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。

36、哪些操作造成内存泄露?

  1. 意外的全局变量
  2. 被遗忘的计时器或者回调函数
  3. 脱离DOM 的引用
  4. 闭包
    第一种情况是我们由于使用未声明的变量而意外的创建了一个全局变量,而使这个变量一直保存再内存中无法被回收。
    第二种情况是我们设置了setTimeout的定时器,而忘记取消了它,如果循环函数有外部变量的引用的话,那么这个变量会被一直保存在内存中而无法被回收
    第三种情况是我们呢获取一个DOM元素的引用,而后面这个元素被删除 ,由于我们一直保留了对这个元素的引用,所以无法被回收
    第四种情况就是不合理使用闭包,从而导致某些变量一直被留在了内存当中,
outerFun(outerArgument){
    //被包含的内部函数可以访问外部函数的变量
    return function(){  
        return outerArgument+1
    } 
} 
// 创建函数 还未调用
var creatFun = outerFun(7)
// 调用函数
var result = creatFun(8)
// 解除对匿名函数的引用
creatFun = null

如果不解除引用的话,已经运行结束的上下文中的变量继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收,解除引用的话可以释放内存
还有一种情况可以防止内存泄漏,利用匿名函数制造了私有作用域,执行完成之后就会将引用的活动对象销毁

function outputNumbers(count){
    // 不用return function,不能引用内部的匿名函数
    // 匿名函数立即执行
    (function(){
        for(var i = 0; i < count; i++){
            console.log(i)
        }
    })() 
    console.log(i) // 出错,因为函数执行完后i变量已销毁
}

**

37-45为ECMAScript 2015(ES6)的常考基础点

**

37、ECMAScript是什么?

ECMAScript是编写语言的标准,这意味着JS遵循ECMAScript标准中的规范变化,因为它是JS的蓝图。
ECMAScript和JS本质上都跟一门语言有关,一个是语言本身的名字开一个是语言的约束条件只不过发明JavaScript的那个人(Netscape公司),把东西交给了ECMA(European Computer Manufacturers Association),这个人规定一下他的标准,因为当时有java语言了,又想强调这个东西是让ECMA这个人定的规则,所以就这样一个神奇的东西诞生了,这个东西的名称就叫做ECMAScript。
javasctipt=ECMAScript+DOM+BOM(一种广义的Javascript)
ECMAScript说什么JavaScript就得做什么!

JavaScript(狭义的JavaScript)做什么都要问问ECMAScript我能不能这样干!如果不能我就错了!能我就是对的!
——突然感觉JavaScript好没有尊严,为啥要搞个人出来约束自己,

那个人被创造出来也好委屈,自己被创造出来完全是因为要约束JavaScript。

38、ECMAScript 2015 (ES6)有哪些特性?

  1. 块级作用域
  2. 箭头函数
  3. 模板字符串
  4. 加强的对象字面量
  5. 对象解构
  6. Promise
  7. 模块
  8. Symbol
  9. 代理Proxy、Set
  10. 函数默认参数
  11. rest和展开运算符

39、let、var、const的区别?

var声明的变量会挂载在window上,而let和const声明的变量不会

var a = 100;
console.log(a,window.a);    // 100 100

let b = 10;
console.log(b,window.b);    // 10 undefined

const c = 1;
console.log(c,window.c);    // 1 undefined

var声明的变量存在变量提升,let和const不存在变量提升

console.log(a); // undefined  ===>  a已声明还没赋值,默认得到undefined值
var a = 100;

console.log(b); // 报错:b is not defined  ===> 找不到b这个变量
let b = 10;

console.log(c); // 报错:c is not defined  ===> 找不到c这个变量
const c = 10;

let和const声明形成块级作用域

if(1){
  var a = 100;
  let b = 10;
}

console.log(a); // 100
console.log(b)  // 报错:b is not defined  ===> 找不到b这个变量

---------------------
if(1){
  var a = 100;
  let b = 10;
}

console.log(a); // 100
console.log(b)  // 报错:b is not defined  ===> 找不到b这个变量

---------------------

同一作用域下,Let和const不能声明同名变量,而var可以

var a = 100;
console.log(a); // 100

var a = 10;
console.log(a); // 10
-------------------------------------
let a = 100;
let a = 10;

//  控制台报错:Identifier 'a' has already been declared  ===> 标识符a已经被声明了。

let和const存在暂时性死区
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var a = 100;

if(1){
    a = 10;
    //在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
    // 而这时,还未到声明时候,所以控制台Error:a is not defined
    let a = 1;
}

const一旦声明必须赋值,不能使用null占位,声明后的值不能修改,如果声明的是复合类型的数据,可以修改其属性

const a = 100; 

const list = [];
list[0] = 10;
console.log(list);  // [10]

const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj);  // {a:10000,name:'apple'}

40、什么是箭头函数?

箭头函数表达式的语法比函数表达式更加简洁,并且没有自己的this、arguments、super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

//ES5 Version
var getCurrentDate = function (){
  return new Date();
}

//ES6 Version
const getCurrentDate = () => new Date();

在本例中,ES5版本有 function(){}声明和return关键字,这两个关键字分别是创建函数和返回函数值所需要的。在箭头函数版本中,我们只需要()括号,不需要return语句,因为如果我们只有一个表达式或者值需要返回,箭头函数就会有一个隐式返回。

//ES5 Version
function greet(name) {
  return 'Hello ' + name + '!';
}

//ES6 Version
const greet = (name) => `Hello ${name}`;
const greet2 = name => `Hello ${name}`;

我们还可以在箭头函数中使用与函数表达式和函数声明相同的参数,如果我们在一个箭头函数中有一个参数,则可以省略括号

const getArgs = () => arguments

const getArgs2 = (...rest) => rest

箭头函数不能访问arguments对象。所以调用第一个getArgs函数会抛出一个错误。相反,我们可以使用rest参数来获得在箭头函数中传递的所有参数。

const data = {
  result: 0,
  nums: [1, 2, 3, 4, 5],
  computeResult() {
    // 这里的“this”指的是“data”对象
    const addAll = () => {
      return this.nums.reduce((total, cur) => total + cur, 0)
    };
    this.result = addAll();
  }
};

箭头函数没有自己的this值,它捕获的是当前作用域函数中的this,在此示例中,addAll函数将computeResult方法中的this值,如果我们在全局作用域声明箭头函数,则this值为window对象

41、什么是类?

类是在JS中编写构造函数的新方法,它是使用构造函数的语法糖,在底层中使用任然是原型和基于原型的继承

 //ES5 Version
   function Person(firstName, lastName, age, address){
      this.firstName = firstName;
      this.lastName = lastName;
      this.age = age;
      this.address = address;
   }

   Person.self = function(){
     return this;
   }

   Person.prototype.toString = function(){
     return "[object Person]";
   }

   Person.prototype.getFullName = function (){
     return this.firstName + " " + this.lastName;
   }  

   //ES6 Version
   class Person {
        constructor(firstName, lastName, age, address){
            this.lastName = lastName;
            this.firstName = firstName;
            this.age = age;
            this.address = address;
        }

        static self() {
           return this;
        }

        toString(){
           return "[object Person]";
        }

        getFullName(){
           return `${this.firstName} ${this.lastName}`;
        }
   }

重写方法并从另一个类继承。

//ES5 Version
Employee.prototype = Object.create(Person.prototype);

function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
  Person.call(this, firstName, lastName, age, address);
  this.jobTitle = jobTitle;
  this.yearStarted = yearStarted;
}

Employee.prototype.describe = function () {
  return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
}

Employee.prototype.toString = function () {
  return "[object Employee]";
}

//ES6 Version
class Employee extends Person { //Inherits from "Person" class
  constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
    super(firstName, lastName, age, address);
    this.jobTitle = jobTitle;
    this.yearStarted = yearStarted;
  }

  describe() {
    return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
  }

  toString() { // Overriding the "toString" method of "Person"
    return "[object Employee]";
  }
}

所以我们要怎么知道它在内部使用原型?

class Something {

}

function AnotherSomething(){

}
const as = new AnotherSomething();
const s = new Something();

console.log(typeof Something); // "function"
console.log(typeof AnotherSomething); // "function"
console.log(as.toString()); // "[object Object]"
console.log(as.toString()); // "[object Object]"
console.log(as.toString === Object.prototype.toString); // true
console.log(s.toString === Object.prototype.toString); // true

42、什么是模板字符串?

模板字符串是在JS中创建字符串的一种新方法,我么可以通过使用反引号使模板字符串化。

//ES5 Version
var greet = 'Hi I\'m Mark';

//ES6 Version
let greet = `Hi I'm Mark`;

在ES5中我们需要使用一些转义字符来达到多行的效果,在模板字符串中没必要这么麻烦。

//ES5 Version
var lastWords = '\n'
  + '   I  \n'
  + '   Am  \n'
  + 'Iron Man \n';


//ES6 Version
let lastWords = `
    I
    Am
  Iron Man   
`;

在ES5版本中,我们需要添加\n以在字符串中添加新行,在模板字符串中,我们不需要这么做。

//ES5 Version
function greet(name) {
  return 'Hello ' + name + '!';
}


//ES6 Version
function greet(name) {
  return `Hello ${name} !`;
}

在ES5版本中,如果需要在字符串中添加表达式或值,则需要使用+运算符,在模板字符串中,我们可以使用${}来嵌入表达式,这样比ES5版本更加简洁。

43、什么是对象解构?

对象解构就是从对象或者数组中获取或者提取值得一种新的更加简洁得方法。
假设有如下对象:

const employee = {
  firstName: "Marko",
  lastName: "Polo",
  position: "Software Developer",
  yearHired: 2017
};

从对象获取属性,早期方法是创建一个与对象属性同名的变量。这种方法很麻烦,因为我们要为每个属性创建一个新变量。假设我们有一个大对象,它有很多属性和方法,用这种方法提取属性会很麻烦。

var firstName = employee.firstName;
var lastName = employee.lastName;
var position = employee.position;
var yearHired = employee.yearHired;

使用解构方式语法就变得简洁多了:

{ firstName, lastName, position, yearHired } = employee;

我们还可以为属性取别名:

let { firstName: fName, lastName: lName, position, yearHired } = employee;

当然如果属性值为 undefined 时,我们还可以指定默认值:

let { firstName = "Mark", lastName: lName, position, yearHired } = employee;

44、什么使Set对象,它是如何工作的?

Set对象允许你存储任何类型得到唯一值,无论是原始值或者是对象引用。
我们可以使用Set构造函数来创建Set实例。

const set1 = new Set();
const set2 = new Set(["a","b","c","d","d","e"]);

我们可以使用add方法向Set实例中添加一个新值,因为add方法返回Set对象,所以我们可以以链式的方式再次使用add。如果一个值已经存在于Set对象中,那么它将不再被添加。

set2.add("f");
set2.add("g").add("h").add("i").add("j").add("k").add("k");
// 后一个“k”不会被添加到set对象中,因为它已经存在了

我们可以使用has方法检查Set实例中是否存在特定的值

set2.has("a") // true
set2.has("z") // true

我们可以使用size属性获得Set实例的长度。

set2.size // returns 10

可以使用clear方法删除 Set 中的数据。

set2.clear();

我们可以使用Set对象来删除数组中重复的元素。

const numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
const uniqueNums = [...new Set(numbers)]; // [1,2,3,4,5,6,7,8]

另外还有WeakSet,与Set类似,也是不重复值得集合,但是WeakSet得成员只能是对象,而不能是其它类型得值。WeakSet中的对象都是弱引用,就是垃圾回收机制不考虑WeakSet对该对象得引用。
Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值都可以当作键。
WeakMap结构与Map结构类似,也是用于生成键值对的集合。但是WeakMap只接受对象作为键名,不接受其它任何类型的值作为键名,而且WeakMap的键名所指向的对象不计入垃圾回收机制。

45、什么是Proxy?

poxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,就是对编程语言进行编程。
Proxy可以理解成在目标对象之前设置一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和修改,Proxy的原意是代理,用在这里表示由它来代理某些操作,可以译为代理器。
以下为高级知识以及相关手写的实现

46、写一个通用的事件侦听器函数?

 const EventUtils = {
  // 视能力分别使用dom0||dom2||IE方式 来绑定事件
  // 添加事件
  addEvent: function(element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent("on" + type, handler);
    } else {
      element["on" + type] = handler;
    }
  },

  // 移除事件
  removeEvent: function(element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent("on" + type, handler);
    } else {
      element["on" + type] = null;
    }
  },

  // 获取事件目标
  getTarget: function(event) {
    return event.target || event.srcElement;
  },

  // 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
  getEvent: function(event) {
    return event || window.event;
  },

  // 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  },

  // 取消事件的默认行为
  preventDefault: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  }
};

47、什么是函数式编程?JS的哪些特性使其成为函数式语言的候选语言?

函数式编程(FP)是通过编写纯函数,避免共享状态、可变数据、副作用来构建软件的过程。函数式编程是声明式的而不是命令式的,应用程序的状态是通过纯函数流动的。与面向对象编程形成对比,面向对象中应用程序的状态通常与对象中的方法共享和共处。

函数式编程是一种编程范式,这意味着它是一种基于一些基本的定于原则(如上所列)思考软件构建的方式。当然,编程范式的其它示例也包括面向对象编程和过程编程。

函数式的代码往往比命令式或面向对象的代码更简洁,更可预测,更容易测试,但是如果不熟悉与之相关的常见模式,函数式的代码也可能看起来更密集杂乱,并且 相关文献对新人来说是不好理解的。

48、什么是高阶函数?

高阶函数只是将函数作为参数或返回值的函数。

function higherOrderFunction(param,callback){
    return callback(param);
}

49、为什么函数被称为一等公民?
在JS中,函数不仅拥有一切传统函数的使用方式(声明和调用),而且还可以做到像简单值一样:

  • 赋值(var fun=function(){})
  • 传参(function func(x,callback){callback();})
  • 返回(function(){return function(){}})
    这样的函数也称之为第一级函数,不仅如此,JS中二等函数还充当了类的构造函数的作用,同时又是一个Function类的实例,这样的多重身份让JS的函数变得非常重要

50、手动实现Array.prototype.map方法

map方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

function map(arr, mapCallback) {
  // 首先,检查传递的参数是否正确。
  if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') { 
    return [];
  } else {
    let result = [];
    // 每次调用此函数时,我们都会创建一个 result 数组
    // 因为我们不想改变原始数组。
    for (let i = 0, len = arr.length; i < len; i++) {
      result.push(mapCallback(arr[i], i, arr)); 
      // 将 mapCallback 返回的结果 push 到 result 数组中
    }
    return result;
  }
}

51、手动实现Array.prototype.filter方法

filter()方法创建一个新数组,其包含了通过所提供函数实现的测试的所有元素。

function filter(arr, filterCallback) {
  // 首先,检查传递的参数是否正确。
  if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function') 
  {
    return [];
  } else {
    let result = [];
     // 每次调用此函数时,我们都会创建一个 result 数组
     // 因为我们不想改变原始数组。
    for (let i = 0, len = arr.length; i < len; i++) {
      // 检查 filterCallback 的返回值是否是真值
      if (filterCallback(arr[i], i, arr)) { 
      // 如果条件为真,则将数组元素 push 到 result 中
        result.push(arr[i]);
      }
    }
    return result; // return the result array
  }
}

52、手动实现Array.prototype.reduce方法

reduce()方法对数组中的每个元素执行一个由自己提供的reducer函数(升序执行),将其结果汇总为单个返回值。

function reduce(arr, reduceCallback, initialValue) {
  // 首先,检查传递的参数是否正确。
  if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function') 
  {
    return [];
  } else {
    // 如果没有将initialValue传递给该函数,我们将使用第一个数组项作为initialValue
    let hasInitialValue = initialValue !== undefined;
    let value = hasInitialValue ? initialValue : arr[0];// 如果有传递 initialValue,则索引从 1 开始,否则从 0 开始
    for (let i = hasInitialValue ? 0 : 1, len = arr.length; i < len; i++) {
      value = reduceCallback(value, arr[i], i, arr); 
    }
    return value;
  }
}

53、js的深浅拷贝

深浅拷贝面试常考!!
**浅拷贝:**创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝,如果属性是基本类型,拷贝的是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址就会影响到另一个对象。
**深拷贝:**将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新的对象,且修改新对象不会影响原对象。

浅拷贝的实现方式:

  1. Object.assign()方法:用于将所有可枚举属性的值从一个或多个源对象复制到目标对象它将返回目标对象。
  2. Array.prototype.slice():slice()方法返回一个新的数组对象,这一个对象是一个由begin和end(不包括end)决定的原数组的浅拷贝,原始的数组不会改变。
  3. 扩展运算符…
let a = {
    name: "Jake",
    flag: {
        title: "better day by day",
        time: "2020-05-31"
    }
}
let b = {...a};

深拷贝的实现方式:

  1. 乞丐版:JSON.parse(JSON.stringify(Object)),缺点诸多(会忽略undefined、symbol、函数,不能解决循环引用,不能处理正则,new Date())
  2. 基础版(面试够用):
    浅拷贝+递归(只考虑了obhect和array两种数据类型)
function cloneDeep(target,map = new WeakMap()) {
  if(typeOf taret ==='object'){
     let cloneTarget = Array.isArray(target) ? [] : {};

     if(map.get(target)) {
        return target;
    }
     map.set(target, cloneTarget);
     for(const key in target){
        cloneTarget[key] = cloneDeep(target[key], map);
     }
     return cloneTarget
  }else{
       return target
  }

}
终极版:
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];


function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        iteratee(array[index], index);
    }
    return array;
}

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
    return Object.prototype.toString.call(target);
}

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            if (param) {
                const paramArr = param[0].split(',');
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        case funcTag:
            return cloneFunction(targe);
        default:
            return null;
    }
}

function clone(target, map = new WeakMap()) {

    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    } else {
        return cloneOtherType(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value, map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value, map));
        });
        return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
}

module.exports = {
    clone
};

54、手写call、apply、bind函数

call函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现调用的情况
  2. 判断传入上下文的对象是否存在,如果存在则设置window
  3. 处理传入的参数,截取第一个参数后的所有参数
  4. 将函数作为上下文对象的一个属性
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
// call函数实现
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }

  // 获取参数
  let args = [...arguments].slice(1),
    result = null;

  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;

  // 将调用函数设为对象的方法
  context.fn = this;

  // 调用函数
  result = context.fn(...args);

  // 将属性删除
  delete context.fn;

  return result;
};

## apply函数的实现步骤:

  1. 判断调用的函数是否为函数,即使我们定义在函数的原型上,但是可能出现使用call等方式调用的情况
  2. 判断传入上下文对象是否存在,如果不存在则设置为window
  3. 将函数作为上下文对象的一个属性
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
// apply 函数实现

Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  let result = null;

  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;

  // 将函数设为对象的方法
  context.fn = this;

  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }

  // 将属性删除
  delete context.fn;

  return result;
};

bind函数的实现步骤:

  1. 判断调用的对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用call等方式调用的情况。
  2. 保存对当前函数的引用,获取传入的参数值。
  3. 创建一个函数返回
  4. 函数内部使用apply来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的this给apply调用,其余情况都传入指定的上下文对象
// bind 函数实现
Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;

  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

55、函数柯里化的实现

函数柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

// 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

function curry(fn, args) {
  // 获取函数需要的参数长度
  let length = fn.length;

  args = args || [];

  return function() {
    let subArgs = args.slice(0);

    // 拼接得到现有的所有参数
    for (let i = 0; i < arguments.length; i++) {
      subArgs.push(arguments[i]);
    }

    // 判断参数的长度是否已经满足函数所需参数的长度
    if (subArgs.length >= length) {
      // 如果满足,执行函数
      return fn.apply(this, subArgs);
    } else {
      // 如果不满足,递归返回科里化的函数,等待参数的传入
      return curry.call(this, fn, subArgs);
    }
  };
}

// es6 实现
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

参考文章:
《JavaScript 专题之函数柯里化》https://github.com/mqyqingfeng/Blog/issues/42

56、js模拟new操作符的实现

掘金上的回答如下:
new操作符做了哪些事情:

  1. 它创建了一个全新的对象
  2. 它会执行[[prototype]](也就是__proto__)链接
  3. 它使this指向新创建的对象
  4. 通过new创建的每个对象将最终被[[prototype]]链接到这个函数的prototype对象上
  5. 如果函数没有返回对象类型Object(包含Function,Array,Date,RegExg,Error),那么new表达式中的函数调用将返回该对象引用
    说实话,看第一遍,我是不理解的,我需要去理一遍原型及原型链的知识才能理解。所以我觉得MDN对new的解释更容易理解:
    new运算符创建一个用户定义的对象类型的实例或者具有构造函数的内置对象的实例,new关键字会进行如下操作:
    1、创建一个空的简单的JS对象(即{})
    2、链接该对象(设置该对象的构造函数)到另外一个对象
    3、将步骤1新创建的对象作为this的上下文
    4、如果该函数没有返回对象就返回this
    接下来我们看如何实现的:
function Dog(name, color, age) {
  this.name = name;
  this.color = color;
  this.age = age;
}

Dog.prototype={
  getName: function() {
    return this.name
  }
}

var dog = new Dog('大黄', 'yellow', 3)

上面的代码相信不用解释,大家都懂。我们来看最后一行带new关键字的代码,按照上述的1,2,3,4步来解析new背后的操作。
第一步:创建一个简单的空对象

var obj = {}

第二步:链接该对象到另外一个对象(原型链)

obj.prototype=Dog.prototype

第三步:将步骤1新创建的对象作为this的上下文
this指向obj对象

Dog.apply(obj,['大黄','yellow',3])

第四步:如果该函数没有返回对象,则返回this

// 因为 Dog() 没有返回值,所以返回obj
var dog = obj
dog.getName() // '大黄'

需要注意的是如果Dog()有return则返回return的值

var rtnObj = {}
function Dog(name, color, age) {
  // ...
  //返回一个对象
  return rtnObj
}

var dog = new Dog('大黄', 'yellow', 3)
console.log(dog === rtnObj) // true

接下来我们将以上步骤封装成一个对象实例化方法,即模拟new的操作

function objectFactory(){
    var obj = {};
    //取得该方法的第一个参数(并删除第一个参数),该参数是构造函数
    var Constructor = [].shift.apply(arguments);
    //将新对象的内部属性__proto__指向构造函数的原型,这样新对象就可以访问原型中的属性和方法
    obj.__proto__ = Constructor.prototype;
    //取得构造函数的返回值
    var ret = Constructor.apply(obj, arguments);
    //如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
    return typeof ret === "object" ? ret : obj;
}

57、什么是回调函数?回调函数有什么缺点?

回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段回调函数的代码。

在JS中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。

const btnAdd = document.getElementById('btnAdd');

btnAdd.addEventListener('click', function clickCallback(e) {
    // do something useless
});

在本例中,我们等待id为btnAdd的元素中的click事件,如果他被单击,就执行clickCallback函数,回调函数向某些数据或事件添加一些功能。
回调函数有一个致命的弱点,就是容易写出回调地狱(Callbackhell),假设多个事件存在依赖性

setTimeout(() => {
    console.log(1)
    setTimeout(() => {
        console.log(2)
        setTimeout(() => {
            console.log(3)

        },3000)

    },2000)
},1000)

这就时典型的回调地狱,以上代码看起来不利于阅读和维护,事件一旦多起来更是乱糟糟,所以在ES6中出现了Promise和async/await来解决回调地狱的问题。当然回调函数还存在别的缺点不能使用try/catch来捕捉错误只能return,接下来两条就是来解决问题的,咱们往下看。

58、Promise是什么,可以手写实现一下吗?

Promise翻译过来是承诺,承诺它过一段时间给你一个结果,从编程讲Promise是异步编程的一种解决方案,下面是Promise在MDN

Promise对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的,它允许你为异步操作的成功和失败分别绑定相应的处理方法,这让异步方法可以像同步方法那样返回值,但并不是最终执行结果,而是一个能代表未来出现结果的Promise对象。

一个Promise有以下几种状态:
pending:初始状态,既不是成功也不是失败状态
fulfilled:意味着操作成功完成
rejected:意味着操作失败

这个承诺一旦从等待状态变为其他状态它就永远不能再改变状态了,也就是说一旦状态变成fulfilled/rejected后就不能在改变,举个简单的例子;

假如我有个女朋友,下周一是她生日,我答应她生日给她一个惊喜,那么从现在开始这个承诺就进入等待状态,等待下周一的到来,然后状态改变。如果下周一我如约给了女朋友惊喜,那么这个承诺的状态就会由pending切换为fulfilled,表示承诺成功兑现,一旦是这个结果了,就不会再有其他结果,即状态不会在发生改变;反之如果当天我因为工作太忙加班,把这事给忘了,说好的惊喜没有兑现,状态就会由pending切换为rejected,时间不可倒流,所以状态也不能再发生变化。

上一条我们说过Promise可以解决地狱回调问题,pending状态的Promise对象会触发fulfilled/rejected状态,一旦状态改变,Promise对象then方法就会被调用,否则会触发catch,我们将上一条回调地狱的代码改一下

new Promise((resolve,reject) => {
     setTimeout(() => {
            console.log(1)
            resolve()
        },1000)

}).then((res) => {
    setTimeout(() => {
            console.log(2)
        },2000)
}).then((res) => {
    setTimeout(() => {
            console.log(3)
        },3000)
}).catch((err) => {
console.log(err)
})

其实Promise也是存在一些问题,比如无法取消Promise,错误需要通过回调函数捕获。

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还有其他的知识,比如Promise.all()、Promise.race()等的运用,由于篇幅原因就不再做展开,想要深入了解的可看下面的文章。

59、iterator是什么,有什么作用?

iterator(迭代器)是一种接口,也可以说是一种规范,为各种不同的数据结构提供统一的访问机制,任何数据结构只要部署了iterator接口就可以完成遍历操作(依次执行该数据节结构里面的所有成员)
iterator语法:

const obj={
	[Symbol.iterator]:function(){}
}

[Symbol.iterator]属性名是固定写法,只要拥有了这个属性的对象就能够使用迭代器的方式进行遍历。

迭代器的遍历方法是首先获得一个迭代器指针,初始时该指针指向第一条数据之前,接着通过调用next方法,改变指针的指向,让其指向下一条数据,每一次的next都会返回一个对象,该对象有两个属性(value和done)
value代表想要获取的数据,done是布尔值,false表示当前指针指向的数据有值,true表示遍历已经结束

Iterator的作用有三个:

  1. 为各种数据结构提供一个统一的简便的接口
    2.使得数据结构的成员能够按照某种次序排列
    ES6创造了一种新的遍历命令for…of循环、Iterator接口主要供for…of消费

遍历过程:

1、创建一个指针对象,指向当前数据结构的起始位置,也就是说,遍历器对象本质上就是一个指针对象。
2、第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
3、第二次调用指针对象的next方法,指针就指向数据结构的第二个成员
4、不断的调用指针对象的next方法,直到它指向数据结构的结束位置

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象,其中,value属性是当前的成员的值,done属性是一个布尔值,表示遍历是否结束

let arr = [{num:1},2,3]
let it = arr[Symbol.iterator]() // 获取数组中的迭代器
console.log(it.next())     // { value: Object { num: 1 }, done: false }
console.log(it.next())     // { value: 2, done: false }
console.log(it.next())     // { value: 3, done: false }
console.log(it.next())     // { value: undefined, done: true }

60、Generator函数是什么,有什么作用

Genertor函数可以说是Iterator接口的具体实现方式,Generator最大的特点就是可以控制函数的执行。

function *foo(x) {
  let y = 2 * (yield (x + 1))
  let z = yield (y / 3)
  return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}

上面这个示例是一个Generator函数,我们来分析其执行过程:

  1. 首先Generator函数调用时它会返回一个迭代器
  2. 当执行第一次next时,传参会被忽略,并且函数暂停在yield(x+1)处,并且返回5+1=6
  3. 当执行第二次next的时候,传入的参数等于上一个yield的返回值,如果你不传参,yield永远返回undefined。此时y=212,所以第二个yield等于212/3=8
  4. 当执行第三次next时,传入的参数会传递给z,所以z = 13, x = 5, y = 24,相加等于 42
    Generator函数一般见到的不多,并且一般去配合co库使用,当然我们可以利用generator函数解决回调地狱的问题。

61、什么是async/await以及如何工作,有什么优缺点?

async/await是一种建立在Promise上的编写异步或非阻塞代码的新方法,被普遍认为是JS异步操作的最终最优雅的解决方案,相对于Promise和回调,它的可读性和简洁度更高,毕竟一直then()很麻烦
async是异步的意思,而await是async wait的简写,即异步等待
所以从语义上很好理解async用于声明一个function是异步的,而await用于等待一个异步方法执行完成
一个函数如果加上async那么该函数就会返回一个promise

async function test(){
	return "1"
}
console.log(test());//Promise{:"1"}

可以看到输出的是一个Promise对象,所以,async函数返回的是一个Promisee对象,如果在async函数中直接return 一个直接量,async会把这个直接量通过Promise.resolve()封装成Promise对象返回,相比于Promise,async/await能更好的处理then链

function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在分别用Promise和async/await来实现这三个步骤的处理
使用Promise:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
        });
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900

使用async/await

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
}
doIt();

结果和之前的Promise实现是一样的,但是这个代码看起来是不是清楚多了,几乎和同步代码一样,await关键字只能在async function中使用,在任何非async function中使用await会抛出错误,await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise对象)返回。
优缺点:
async/await的优势在于处理then的调用链,能够更清晰的写出代码,并且能优雅的解决回调地狱问题,当然也存在一些缺点因为await将异步代码改造成同步代码。如果多个异步代码没有依赖性却使用了await会导致性能下降

62、instanceof的原理是什么,如何实现

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

  1. 首先获取类型的原型
  2. 然后获取对象的原型
  3. 然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为null,因为原型链最终为null
function myInstanceof(left, right) {
  let prototype = right.prototype
  left = left.__proto__
  while (true) {
    if (left === null || left === undefined)
      return false
    if (prototype === left)
      return true
    left = left.__proto__
  }
}

63、js的节流与防抖

函数防抖是指在事件被触发n秒后在执行回调,如果在这n秒内事件又被触发,则重新计时,这可以使用在一些点击请求的时间上,避免因为用户的多次点击向后端发送多次请求。

函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某时间被触发多次,只有一次能生效,节流可以使用scroll函数的事件监听上,通过事件节流来降低调用的频率。简而言之就是每隔一段时间只执行一次函数

函数防抖的应用场景:
1、搜索框的搜索输入,只有用户输入完毕才发送请求
2、手机号、邮箱的验证输入检测
3、窗口大小resize,只要窗口大小调整完毕之后,计算窗口大小,防止重复渲染

函数节流的应用场景:

1、滚动加载,加载更多或滚到底部监听
2、谷歌搜索框,搜索联想功能
3、高频点击提交,表单重复提交

异同比较:
相同点:
1、都可以使用setTimeout来实现
2、目的都是降低回调的执行频率 节省计算资源
不同点:
1、函数防抖,在一段连续操作结束之后处理回调,利用clearTimeout和setTimeout实现,函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
2、函数防抖关注的是一定时间连续触发,只在最后一次执行,而函数节流侧重的是一段时间内只执行一次

// 函数防抖的实现
function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = arguments;

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

// 函数节流的实现;
function throttle(fn, delay) {
  var preTime = Date.now();

  return function() {
    var context = this,
      args = arguments,
      nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - preTime >= delay) {
      preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

64、什么是设计模式?

1、概念
设计模式是一套被反复使用的,多数人知晓的,经过分类编目的、代码设计经验的总结,使用设计模式是为了重用代码、让代码更容易被他人理解,保证代码可靠性。毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
2、设计原则

  1. S-单一职责原则
    (1)一个程序只做好一件事情
    (2) 如果功能过于复杂就拆分开来,每个部分保持独立

  2. O-开放封闭原则
    (1)对扩展开放,对修改封闭
    (2)增加需求时,扩展新代码,而非修改已有的代码

  3. L-里氏替换规则
    (1)子类能够替换父类
    (2)父类能出现的地方子类就能出现

  4. 接口隔离原则
    (1)保持接口的单一独立
    (2)类似单一职责原则,这里更关注接口

  5. D-依赖倒转原则
    (1)面向接口编程,依赖于抽象而不依赖于具体
    (2)使用方只关注接口而不关注具体类的实现

3、设计模式的类型
(1)结构型模式(Structural Patterns): 通过识别系统中组件间的简单关系来简化系统的设计。
(2)创建型模式(Creational Patterns): 处理对象的创建,根据实际情况使用合适的方式创建对象。常规的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。
(3)行为型模式(Behavioral Patterns): 用于识别对象之间常见的交互模式并加以实现,如此,增加了这些交互的灵活性。

65、9种常见的设计模式

1、外观模式
外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API。很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已。
(1)兼容浏览器事件绑定

let addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn
    }
}; 

(2)封装接口

let myEvent = {
    // ...
    stop: e => {
        e.stopPropagation();
        e.preventDefault();
    }
};

场景

  • 设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade

在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。
在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观
Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。
优点

  1. 减少系统相互依赖。
  2. 提高灵活性。
  3. 提高了安全性
    缺点
    不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

2. 代理模式(Proxy Pattern)
是为一个对象提供一个代用品或占位符,以便控制对它的访问

假设当A 在心情好的时候收到花,小明表白成功的几率有
60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。
小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花
被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。
但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选
择A 心情好的时候把花转交给A,代码如下:

let Flower = function() {}
let xiaoming = {
  sendFlower: function(target) {
    let flower = new Flower()
    target.receiveFlower(flower)
  }
}
let B = {
  receiveFlower: function(flower) {
    A.listenGoodMood(function() {
      A.receiveFlower(flower)
    })
  }
}
let A = {
  receiveFlower: function(flower) {
    console.log('收到花'+ flower)
  },
  listenGoodMood: function(fn) {
    setTimeout(function() {
      fn()
    }, 1000)
  }
}
xiaoming.sendFlower(B)

场景:
(1)HTML元 素事件代理

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
<script>
  let ul = document.querySelector('#ul');
  ul.addEventListener('click', event => {
    console.log(event.target);
  });
</script>

(2)ES6 的 proxy 阮一峰Proxy
(3)jQuery.proxy()方法
优点
1、代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
2、代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
缺点
处理请求速度可能有差别,非直接访问存在开销
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()

场景
(1)如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
(2)将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
(3)需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性
优点

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

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

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

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

缺点

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

考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
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;
    }
  }
})();

实现的关键点有:

使用 IIFE创建局部作用域并即时执行;
getInstance()为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回。
我们可以验证下单例对象是否创建成功:

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

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

场景例子

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

登录框

vuex 和 redux中的store
优点
划分命名空间,减少全局变量

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

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

缺点

由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合
从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一
个单元一起测试。

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

<html>
<head>
    <title>策略模式-校验表单</title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">
        用户名:<input type="text" name="userName">
        密码:<input type="text" name="password">
        手机号码:<input type="text" name="phoneNumber">
        <button type="submit">提交</button>
    </form>
    <script type="text/javascript">
        // 策略对象
        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
          }
        }
    </script>
</body>
</html>

场景例子

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

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

表单验证

优点

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

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

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

缺点

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

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

  1. 迭代器模式(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!

优点

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

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

增加了灵活性

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

缺点

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

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,并通过调用Visitor的 visit()
    为其提供获取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)