【考前必看一】面试前夕知识点梳理之JavaScript

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Dora_5537/article/details/99292627

一、JavaScript

1. 构造函数

  • 构造函数是一种特殊的方法,主要用来创建对象时初始化对象,总与new运算符一起使用。

2. new运算符

  • new运算符会创建一个空对象,并且构造函数中的 this 指向这个空对象
  • 这个新对象会被执行[[原型]]连接,即连接构造函数的原型
  • 执行构造函数,将属性和方法添加到 this 引用的对象中,即创建的这个新对象
  • 如果构造函数中没有返回其它对象,那么返回 this,即创建的这个新对象;否则,返回构造函数中返回的对象
function _new() {
    let target = {}; // 创建的新对象
    // 第一个参数是构造函数
    let [constructor, ...args] = [...arguments];
    // 执行 [[原型]] 连接 ;target 是 constructor 的实例
    target.__proto__ = constructor.prototype;
    // 执行构造函数,将属性或方法添加到创建的空对象上
    let result = constructor.apply(target, args);
    if (result && (typeof (result) == "object" || typeof (result) == "function")) {
        // 如果构造函数执行的结构返回的是一个对象,那么返回这个对象
        return result;
    }
    // 如果构造函数返回的不是一个对象,返回创建的新对象
    return target;
}

3. 原型和原型链

在JavaScript中,每个对象都可以称之为原型。原型的值可以是一个对象,也可以是null

  • 原型对象都包含一个指向构造函数的指针,同时构造函数也有一个内部属性prototype,这个内部属性是一个指针,指向该原型对象。注:有且仅有函数才有prototype属性。
  • 通过构造函数可以新建原型对象的实例,所有的对象实例都有一个内部属性[[prototype]],这个内部属性是一个指针,指向原型对象。注:null 没有内部属性[[prototype]]。
  • 原型对象的作用是可以让所有的对象实例共享它所包含的属性方法

如果原型的值是一个对象,那么这个对象也一定有自己的原型。这样就形成了一条线性的链,我们称之为原型链

  • 原型链的尽头是 null,即一个空对象
  • 原型链的作用是用来实现继承。比如我们新建一个数组,数组的方法就是从数组的原型上继承而来的。

4. 继承

ES5 一共有有六种方式可以实现继承,分别为:原型链继承、借用构造函数、组合继承 (原型链 + 借用构造函数)、原型式继承、寄生式继承、寄生组合式继承。

第一种方式:原型链继承

  • 原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法
function SuperType() {
    this.name = 'Yvette';
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
    return this.name;
}
function SubType() {
    this.age = 22;
}
SubType.prototype = new SuperType();
SubType.prototype.getAge = function() {
    return this.age;
}
SubType.prototype.constructor = SubType;
let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Yvette'
console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

let instance2 = new SubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

缺点:

  • 通过原型来实现继承时,原型会变成另一个原型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享。
  • 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。

第二种方式:借用构造函数

  • 借用构造函数的基本思想是在子类型的构造函数中调用超类型的构造函数
function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
function SubType(name) {
    SuperType.call(this, name);
}
let instance1 = new SubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]

let instance2 = new SubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']

优点:

  • 可以向超类传递参数。
  • 解决了原型中包含引用类型值被所有实例共享的问题。

缺点:

  • 方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。

第三种方式:组合继承 (原型链 + 借用构造函数)

  • 组合继承的基本思想是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。
function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SuberType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;
SuberType.prototype.sayAge = function () {
    console.log(this.age);
}
let instance1 = new SuberType('Yvette', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Yvette

let instance2 = new SuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//Jack

缺点:

  • 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

优点:

  • 可以向超类传递参数。
  • 每个实例都有自己的属性。
  • 实现了函数复用。

第四种方式:原型式继承

  • 原型式继承的基本思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
function object(o) {
    function F() { }
    F.prototype = o;
    return new F();
}

在 object() 函数内部,先穿甲一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例,从本质上讲,object() 对传入的对象执行了一次浅拷贝。 

ECMAScript5 通过新增 Object.create() 方法规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象 (可以覆盖原型对象上的同名属性),在传入一个参数的情况下,Object.create() 和 object() 方法的行为相同。

var person = {
    name: 'Yvette',
    hobbies: ['reading', 'photography']
}
var person1 = Object.create(person);
person1.name = 'Jack';
person1.hobbies.push('coding');
var person2 = Object.create(person);
person2.name = 'Echo';
person2.hobbies.push('running');
console.log(person.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]
console.log(person1.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]

 在没有必要创建构造函数,仅让一个对象与另一个对象保持相似的情况下,原型式继承是可以胜任的。

缺点:

  • 同原型链继承一样,包含引用类型值的属性会被所有实例共享。

第五种方式:寄生式继承

  • 寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象
function createAnother(original) {
    var clone = object(original);// 通过调用函数创建一个新对象
    clone.sayHi = function () {  // 以某种方式增强这个对象
        console.log('hi');
    };
    return clone;// 返回这个对象
}
var person = {
    name: 'Yvette',
    hobbies: ['reading', 'photography']
};

var person2 = createAnother(person);
person2.sayHi(); // hi

基于 person 返回了一个新对象 —— person2,新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi() 方法。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

缺点:

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。
  • 同原型链继承一样,包含引用类型值的属性会被所有实例共享。

第六种方式:寄生组合式继承

  • 寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法
  • 寄生组合式继承的基本思想是不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

寄生组合式继承的基本模式如下所示:

function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType;// 增强对象
    subType.prototype = prototype;// 指定对象
}
  • 第一步:创建超类型原型的一个副本。

  • 第二步:为创建的副本添加 constructor 属性。

  • 第三步:将新创建的对象赋值给子类型的原型。

至此,我们就可以通过调用 inheritPrototype 来替换为子类型原型赋值的语句:

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
//...code
function SuberType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SuberType.prototype = new SuperType();
inheritPrototype(SuberType, SuperType);
//...code

优点:

  • 只调用了一次超类构造函数,效率更高。避免在SuberType.prototype上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。
  • 因此寄生组合式继承是引用类型最理性的继承范式。

5. 作用域和作用域链

作用域分为全局作用域函数作用域块级作用域

作用域链就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链。例如:

let a = 10;
function fn1() {
    let b = 20;
    function fn2() {
        a = 20
    };
    return fn2;
}
fn1()();

fn2 作用域链 = [ fn2 作用域,fn1 作用域,全局作用域 ]

6. this对象

this 是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用

this 的指向:谁调用它,this 就指向谁。分为三种情况:

  • 纯粹的函数调用:如果是一般的函数调用,this指向全局对象;在严格模式"use strict"下,为undefined。
  • 作为对象方法的调用:如果是在对象的方法里调用,this指向调用该方法的对象。
  • 作为构造函数调用:如果是在构造函数里调用,this指向创建出来的实例对象。

改变this指向的四种方法:

  • 使用 that 
  • 使用.bind():Object.bind(this,obj1,obj2,obj3) 。
  • 使用.apply():Object.apply(this,arguments) 。
  • 使用.call():Object.call(this,obj1,obj2,obj3) 。

注意:bind()方法只会返回一个函数,并不会执行函数,而 apply() 和 call() 会立即执行函数。

箭头函数的情况:箭头函数没有自己的 this,继承外层上下文绑定的 this。

let obj = {
    age: 20,
    info: function() {
        return () => {
            console.log(this.age); //this 继承的是外层上下文绑定的 this
        }
    }
}

let person = {age: 28};
let info = obj.info();
info(); //20

let info2 = obj.info.call(person);
info2(); //28

7. 闭包

闭包的定义:闭包是指有权访问另一个函数作用域中的变量的函数。

创建一个闭包:闭包使得函数可以继续访问定义时的词法作用域。拜 fn 所赐,在 foo() 执行后,foo 内部作用域不会被销毁。 

function foo() {
    var a = 2;
    return function fn() {
        console.log(a);
    }
}
let func = foo();
func(); // 输出 2

闭包的作用

  • 能够访问函数定义时所在的词法作用域 (阻止其被回收)。
  • 私有化变量
function base() {
    let x = 10; // 私有变量
    return {
        getX: function() {
            return x;
        }
    }
}
let obj = base();
console.log(obj.getX()); //10
  • 模拟块级作用域
var a = [];
for (var i = 0; i < 10; i++) {
    a[i] = (function(j){
        return function () {
            console.log(j);
        }
    })(i);
}
a[6](); // 6
  • 创建模块
function coolModule() {
    let name = 'Yvette';
    let age = 20;
    function sayName() {
        console.log(name);
    }
    function sayAge() {
        console.log(age);
    }
    return {
        sayName,
        sayAge
    }
}
let info = coolModule();
info.sayName(); //'Yvette'

模块模式具有两个必备的条件 (来自《你不知道的 JavaScript》)

  • 必须有外部的封闭函数,该函数必须至少被调用一次 (每次调用都会创建一个新的模块实例)。

  • 封闭函数必须返回至少 一个 内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。 

8. 同步和异步 以及 JS的执行机制

同步可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。

异步执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。(当一个异步过程调用发出后,调用者不能立刻得到结果;实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者)

异步编程的方法:回调函数、事件监听、发布/订阅、Promises对象。

  • 方法1:回调函数(callbacks)。优点是简单、容易理解和部署。缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。
  • 方法2:事件监听。可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
  • 方法3:发布/订阅。性质与“事件监听”类似,但是明显优于后者。
  • 方法4:Promises对象。它是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。

异步加载 JS 脚本的方式:async 或者 defer、动态创建 script 标签、XHR 异步加载 JS。

  • 方法1:async 或者 defer。<script> 标签中增加 async (html5) 或者 defer (html4) 属性,脚本就会异步加载。
<script src="../XXX.js" async></script>
<script src="../XXX.js" defer></script>

     defer 和 async 的区别在于:

  1. defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),在 window.onload 之前执行;而 async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。

  2. 如果有多个 defer 脚本,会按照它们在页面出现的顺序加载;而多个 async 脚本不能保证加载顺序。

  • 方法2:动态创建 script 标签。动态创建的 script ,设置 src 并不会开始下载,而是要添加到文档中,JS 文件才会开始下载。
let script = document.createElement('script');
script.src = 'XXX.js';
// 添加到 html 文件中才会开始下载
document.body.append(script);
  • 方法3:XHR 异步加载 JS。
let xhr = new XMLHttpRequest();
xhr.open("get", "js/xxx.js", true);
xhr.send();
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        eval(xhr.responseText);
    }
}

JS的执行机制:

除了广义的同步任务和异步任务,我们对任务有更精细的定义:

  • macro-task (宏任务):整体代码script,setTimeout,setInterval
  • micro-task (微任务):Promise,process.nextTick

事件循环流程:

  • 执行整体代码script,作为第一个宏任务,进入主线程。注意:遇到 setTimeout、setInterval,将其回调函数分发到宏任务Event Queue中。遇到 Promise,new Promise会立即执行一次,然后将 then 分发到微任务Event Queue中。遇到process.nextTick,将其回调函数分发到微任务Event Queue中。
  • 执行微任务Event Queue:Promise、process.nextTick
  • 执行宏任务Event Queue:setTimeout、setInterval

事件循环,宏任务,微任务的关系如图所示:

console.log('1');
 
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
 
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

// 完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

9. 深拷贝和浅拷贝

(1) 基本数据类型不存在深浅拷贝,两者相互独立,互不影响。

当 var a = 1,var b = a 时,相当于在栈内存中新开辟了一个内存,如下所示:

(2) 深拷贝和浅拷贝是针对复杂的引用数据类型(对象类型)来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。

浅拷贝:浅拷贝是会将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。

当 var a = [0,1,2,3,4],var b = a 时,a 和 b 会指向同一个堆地址,所以当我们改变 a 时 b 也会改变,改变 b 时 a 也会改变,如下所示:

深拷贝:深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。

在深拷贝的情况下,我们会为 b 单独开辟一块堆内存,如下所示:

深拷贝的实现:

  • 深拷贝最简单的实现是: JSON.parse(JSON.stringify(obj)),这是最简单的实现方式。但是有一些缺陷:
  1. 对象的属性值是函数时,无法拷贝。
  2. 原型链上的属性无法拷贝。
  3. 不能正确的处理 Date 类型的数据。
  4. 不能处理 RegExp。
  5. 会忽略 symbol。
  6. 会忽略 undefined。
  • 实现一个 deepClone 函数。
  1. 如果是基本数据类型,直接返回。
  2. 如果是 RegExp 或者 Date 类型,返回对应类型。
  3. 如果是复杂数据类型,递归。
  4. 考虑循环引用的问题。
function deepClone(obj, hash = new WeakMap()) { // 递归拷贝
    if (obj instanceof RegExp) return new RegExp(obj);
    if (obj instanceof Date) return new Date(obj);
    if (obj === null || typeof obj !== 'object') {
        // 如果不是复杂数据类型,直接返回
        return obj;
    }
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    /**
     * 如果 obj 是数组,那么 obj.constructor 是 [Function: Array]
     * 如果 obj 是对象,那么 obj.constructor 是 [Function: Object]
     */
    let t = new obj.constructor();
    hash.set(obj, t);
    for (let key in obj) {
        // 递归
        if (obj.hasOwnProperty(key)) {// 是否是自身的属性
            t[key] = deepClone(obj[key], hash);
        }
    }
    return t;
}

10. 事件流、事件冒泡 和 事件捕获、事件委托

事件流:从页面中接收事件的顺序。也就是说当一个事件产生时,这个事件的传播过程,就是事件流。

IE的事件流叫做事件冒泡。事件冒泡:事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。对于HTML来说,就是当一个元素产生了一个事件,它会把这个事件传递给它的父元素,父元素接收到了之后,还要继续传递给它的上一级元素,就这样一直传播到document对象(现在的浏览器到window对象,只有IE8及以下不这样)。

  • 阻止事件冒泡的方法:如果是 IE 浏览器,则使用 window.event.cancelBubble = true;如果是非 IE 浏览器,则使用 e.stopPropagation() 。stopPropagation() 是事件对象(Event)的一个方法,作用是阻止目标元素的冒泡事件,但是不会取消默认行为。
// 阻止冒泡事件
function stopBubble(e){
    // 如果提供了事件对象,则这是一个非IE浏览器
    if(e && e.stopPropagation){
        e.stopPropagation();
    } else {
        // 否则,使用IE的方式来取消事件冒泡
        window.event.cancelBubble = true;
    }
}
  • 取消默认行为的方法:如果是 IE 浏览器,则使用 window.event.returnValue = false;如果是非 IE 浏览器,则使用 e.preventDefault()。preventDefault() 是事件对象(Event)的一个方法,作用是取消目标元素的默认行为。既然是说默认行为,当然是元素必须有默认行为才能被取消,如果元素本身就没有默认行为,调用当然就无效了。
// 取消默认行为
function stopDefault(e){
    if(e && e.preventDefault){
        e.preventDefault();
    } else {
        window.event.returnValue = false;
    }  
    return false;  
}

注意:原生 JavaScript 的 return false 只会取消默认行为;但如果使用的是 jQuery 的话,则既会取消默认行为又可以防止对象冒泡。

事件捕获 的思想是不太具体的元素应该更早接受到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。即和冒泡的过程正好相反。以HTML的click事件为例,document对象(DOM级规范要求从document开始传播,但是现在的浏览器是从window对象开始的)最先接收到click事件,然后事件沿着DOM树依次向下传播,一直传播到事件的实际目标。

事件委托:利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。即把事件加到父级上,触发执行效果。

  • 事件委托的好处:新添加的元素还会有之前的事件。大大减少了DOM操作,提高了性能。
  • 适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。

END

展开阅读全文

没有更多推荐了,返回首页