前端中高级知识要点总结

本文整理前端常见的知识要点,方便随时复盘。内容主要涵盖 JS 基础、CSS、HTML,JS 面向对象,渲染机制,网络 HTTP/通信,前端安全,设计模式,算法等方面。

注:文章内容相对而言有一定的难度,所以需要读者有一定的基础。另外,若文章有何不妥之处或者你有任何疑问,欢迎留言讨论。

一.JS基础

1.手写实现 call()

call()、apply() 和 bind() 这是前端初学者比较头疼的三个方法,也是从初级向中级进阶所必需掌握的。通过使用这些方法,我们可以修改函数绑定的 this,使其成为我们指定的对象。

在实现 call() 之前,先来看下如何使用它。(注:由于 call() 和 apply() 作用类似,所以这里一起介绍)

func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [arg1, arg2, ...]) 

我们可以看到,call() 和 apply() 方法的区别在于其传递参数的格式不同,一个是参数列表,一个是数组

再看个实际的例子

function sum(num1, num2) {
  return num1 + num2;
}
 
function callSum(num1, num2) {
  return sum.call(this, num1, num2);
  // return sum.apply(this, [num1, num2]); // apply 方式调用
}
 
console.log(callSum(10, 10)); // 20

传递参数并非 call() 和 apply() 真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域

window.color = 'red';
var o = { color: 'blue' };
 
function sayColor() {
  console.log(this.color)
}
 
// 注:下面代码中的call也可以换成apply
sayColor();             // red
sayColor.call(this);    // red
sayColor.call(window);  // red
sayColor.call(o);       // blue

这里通过传入 o 对象,使 sayColor() 方法中的this指向了 o,所以最后输出了 blue。

好了,了解了 call() 的使用和原理,开始实现 call() 吧

// 手写call()方法
Function.prototype.myCall = function(thisArg, ...args) {
    thisArg.fn = this              // this指向调用call的对象,即我们要改变this指向的函数
    return thisArg.fn(...args)     // 执行函数并return其执行结果
}

// 尝试第一个例子
function sum(num1, num2) {
  return num1 + num2;
}
 
function callSum(num1, num2) {
  return sum.myCall(this, num1, num2);
}
 
console.log(callSum(10, 10)); // 20

// 尝试第二个例子
window.color = 'red';
var o = { color: 'blue' };
 
function sayColor() {
  console.log(this.color)
}

sayColor();               // red
sayColor.myCall(this);    // red
sayColor.myCall(window);  // red
sayColor.myCall(o);       // blue

可以看到,我们自己实现的 myCall() 方法与原生的 call() 方法执行的效果完全一样。

上面的 call() 实现还能进一步优化

Function.prototype.myCall = function(thisArg, ...args) {
    var fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
    thisArg = thisArg || window    // 若没有传入this, 默认绑定window对象
    thisArg[fn] = this              // this指向调用call的对象,即我们要改变this指向的函数
    var result = thisArg[fn](...args)  // 执行当前函数
    delete thisArg[fn]              // 删除我们声明的fn属性
    return result                  // 返回函数执行结果
}

2.手写实现 apply()

基于上面 call() 的例子,我们这里就直接实现 apply() 方法

// 手写实现apply方法
Function.prototype.myApply = function(thisArg, args) {
    var fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
    thisArg = thisArg || window    // 若没有传入this, 默认绑定window对象
    thisArg[fn] = this              // this指向调用apply的对象,即我们要改变this指向的函数
    var newArgs = args ? args : []  // 这里判断是否传递了参数
    var result = thisArg[fn](...newArgs)  // 执行当前函数(此处说明一下:虽然apply()接收的是一个数组,
    // 但在调用原函数时,依然要展开参数数组。可以对照原生apply(),原函数接收到展开的参数数组)
    delete thisArg[fn]              // 删除我们声明的fn属性
    return result                  // 返回函数执行结果
}

// 尝试第一个例子
function sum(num1, num2) {
  return num1 + num2;
}
 
function callSum(num1, num2) {
  return sum.myApply(this, [num1, num2]);
}
 
console.log(callSum(10, 10)); // 20

// 尝试第二个例子
window.color = 'red';
var o = { color: 'blue' };
 
function sayColor() {
  console.log(this.color)
}

sayColor();                // red
sayColor.myApply(this);    // red
sayColor.myApply(window);  // red
sayColor.myApply(o);       // blue

3.手写实现 bind()

bind() 方法,这个方法会创建一个函数的实例,其 this 值会被绑定到传递给 bind() 函数的值(也就是第一个参数)。

window.color = 'red';
var o = { color: 'blue' };
 
function sayColor() {
  console.log(this.color)
}
 
var objectSayColor = sayColor.bind(o);
objectSayColor();  // blue

其实使用 apply()/call() 方法就可以实现一个 bind():

// 手写bind
Function.prototype.myBind = function(...rest1) {
    var self = this
    var context = rest1.shift() // 取得第一个参数(即执行环境),并删除
    return function(...rest2) {
        return self.apply(context, [...rest1, ...rest2])
    }
}

// 测试
window.color = 'red';
var o = { color: 'blue' };
 
function sayColor() {
  console.log(this.color)
  console.log(arguments)
}
 
var objectSayColor = sayColor.myBind(o, '1', '2');
objectSayColor('3');  // 输出 "blue" 和 "1", "2", "3" 相关的数据

4.手写 Promise

Promise 的实现有一定难度,这里推荐一篇文章:9k字 | Promise/async/Generator实现原理解析

5.闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式:在一个函数内部创建另一个函数。

一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅会保存全局作用域(全局执行环境的变量对象)。但闭包则有所不同。

一个闭包的例子:

function createFunction(width, height) {
  return function(object) {
    var area = object[width] * object[height]
    return area
  }
}
 
// 创建函数
var areaFunc = createFunction('width', 'height');
// 调用函数
var area = areaFunc({ width: 3, height: 2 })

这里的 areaFunc 方法的作用域链中,包含了闭包的活动对象(arguments,object)、createFunction() 的活动对象(arguments, width, height)和全局变量对象(createFunction,area)。当 createFunction() 的函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然留在内存中,这时需要我们手动销毁匿名函数,以释放内存。

// 解除对匿名函数的引用(释放内存)
areaFunc = null;

注:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。所以,我们最好在必要时才使用闭包。 

6.函数柯里化

函数柯里化,用于创建已经设置好了一个或多个参数的函数。

通过下面这个例子能很好的理解其概念。

function add(num1, num2) {
  return num1 + num2;
}

function curriedAdd(num2) {
  return add(5, num2);
}

console.log(add(2, 3));     // 5
console.log(curriedAdd(3)); // 8

这个例子从技术上讲并非柯里化的函数,但很好的展示了其概念。

创建柯里化函数的步骤:调用另一个函数并为它传入要柯里化的函数和必要参数。通用方式如下:

function curry(fn) {
  var args = Array.prototype.slice.call(arguments, 1); // 删除传入的function,获取其余参数
  return function() {
    var innerArgs = Array.prototype.slice.call(arguments); // 取得参数
    var finalArgs = args.concat(innerArgs);
    return fn.apply(null, finalArgs); // 这里不需要考虑执行环境,所以null
  }
}

// 测试
function add(num1, num2) {
  return num1 + num2;
}

var curriedAdd = curry(add, 5);
console.log(curriedAdd(3)); // 8

// 也可以第一次就直接传递完参数
var curriedAdd2 = curry(add, 5, 3);
console.log(curriedAdd2()); // 8

其实 bind() 方法中就实现了函数柯里化,只要在 this 值之后再传入另一个参数即可。 柯里化函数提供了强大的动态函数创建功能,但不要滥用,因为会带来额外的开销。

7.节流函数

防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器。

如果上面的介绍没懂,那举个例子。比如,窗口的 onresize 事件,我们分别使用防抖和节流,时间设为 500 毫秒。使用防抖:若你一直在改变窗口大小,则你的处理方法不会执行,只有你停止改变窗口大小后的 500 毫秒后,才会执行你的处理方法。使用节流:若你一直在改变窗口大小,则每 500 毫秒就会执行一次你的处理方法。

理解完概念,我们在看节流函数如何写的吧:

function throttle(func, wait) {
  let timeout = null
  return function() {
    const context = this
    const args = arguments
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null
        func.apply(context, args)
      }, wait)
    }
  }
}

注:只要代码是周期性执行的,都可以考虑使用节流。 

8.防抖函数

防抖函数如下

function debounce (func, wait) {
  let timeout = null
  return function() {
    const context = this
    const args = arguments
    if (timeout) clearTimeout(timeout)
    timeout = setTimeout(() => {
      func.apply(context, args)
    }, wait)
  }
}

应用场景:比如,搜索框搜索。如果在工作中想直接使用防抖和节流方法,可以使用 Lodash.js 插件(这个插件整合很多常用的工具方法)。

9.数组扁平化

比如要将 [1, [1,2], [1,2,3]] 展开为 [1, 1, 2, 1, 2, 3]

(1)ES6 中的 flat()

const arr = [1, [1,2], [1,2,3]].flat()

(2)使用正则

const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str)

上面这两种方式比较简单实用,当然,还可以使用其他方法,比如循环递归。这里介绍一个 reduce() 的方式

(3)使用 reduce()

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
  return arr.reduce((prev, cur) => {
    return prev.concat(cur instanceof Array ? flat(cur) : cur)
  }, [])
}

flat(arr)

10.深拷贝,浅拷贝

深浅拷贝都是针对引用类型而言的,浅拷贝只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。

【浅拷贝】

浅拷贝只是复制了引用,并没有实现真正的复制。

var arr = [1,2,3];
var obj = {a:'a', b:[1,2], c:{cc:'cc'}};

var cloneArr = arr;
var cloneObj = obj;

cloneArr.push(4);
cloneObj.a = 'aaa';

console.log(arr);      // [1, 2, 3, 4]
console.log(cloneArr); // [1, 2, 3, 4]

console.log(obj);      // {a:'aaa', b:[1,2], c:{cc:'cc'}}
console.log(cloneObj); // {a:'aaa', b:[1,2], c:{cc:'cc'}}

这里,我们无论改变原来的值还是克隆的值,它们都会相互影响,并没有实现隔离。

【深拷贝】

深拷贝则是完完全全的拷贝,它们之间彼此隔离,互不影响。

实现深拷贝的方法主要有两种:

  1. 利用 JSON 对象中的 parse 和 stringify
  2. 利用递归来实现每一层都重新创建对象并赋值

(1)JSON 对象中的 parse 和 stringify

var arr = [1,2,3];
var obj = {a:'a', b:[1,2], c:{cc:'cc'}};

var cloneArr = JSON.parse(JSON.stringify(arr));
var cloneObj = JSON.parse(JSON.stringify(obj));

cloneArr.push(4);
cloneObj.a = 'aaa';
cloneObj.b.push(3);
cloneObj.c = 'ccc';

console.log(arr);      // [1, 2, 3]
console.log(cloneArr); // [1, 2, 3, 4]

console.log(obj);      // {a:'a', b:[1,2], c:{cc:'cc'}}
console.log(cloneObj); // {a:'aaa', b:[1,2,3], c:{cc:'ccc'}}

这个方法确实实现了深拷贝,也是我们最常用的一种方法,但对于一些复杂的引用类型,就存在问题

var obj = {
  name: 'Tom',
  sayName: function(){
    console.log(this.name)
  }
}

var cloneObj = JSON.parse(JSON.stringify(obj));

console.log(obj);      // {name: "Tom", sayName: ƒ}
console.log(cloneObj); // {name: "Tom"}

我们发现,它并没有将方法复制下来。原因是:undefined、function、symbol 会在转换过程中被忽略。。。

(2)递归实现

利用递归实现深拷贝的思想是每一层都重新创建对象并赋值

function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for(let keys in source){
    if(source.hasOwnProperty(keys)){ // 判断属性是否存在于实例中
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}

// 测试
var obj = {
  name: 'Tom',
  sayName: function(){
    console.log(this.name)
  }
}

var cloneObj = deepClone(obj);

console.log(obj);      // {name: "Tom", sayName: ƒ}
console.log(cloneObj); // {name: "Tom", sayName: ƒ}

在来测测最开始的例子

var arr = [1,2,3];
var obj = {a:'a', b:[1,2], c:{cc:'cc'}};

var cloneArr = deepClone(arr);
var cloneObj = deepClone(obj);

cloneArr.push(4);
cloneObj.a = 'aaa';
cloneObj.b.push(3);
cloneObj.c = 'ccc';

console.log(arr);      // [1, 2, 3]
console.log(cloneArr); // [1, 2, 3, 4]

console.log(obj);      // {a:'a', b:[1,2], c:{cc:'cc'}}
console.log(cloneObj); // {a:'aaa', b:[1,2,3], c:{cc:'ccc'}}

我们发现,都是 ok 的。

【JavaScript 中的拷贝方法】

JavaScript中的有的方法也能实现拷贝,比如:concat()和slice(),ES6中的Object.assgin()和...展开运算符。这里就不一一测试了,直接给出结论吧。

  1. concat 只是对数组的第一层进行深拷贝
  2. slice 只是对数组的第一层进行深拷贝
  3. Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值
  4. ... 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值

11.JS 中的 this

(1)this的指向

this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式(调用位置)

(2)this的绑定规则

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定

默认绑定

function foo() {
  console.log( this.a );
}
 
var a = 2;
foo(); // 2

foo 函数调用位置是在全局,因此 this 指向全局对象。foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined。 

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

function foo() {
  console.log( this.a );
}
 
var obj = {
  a: 2,
  foo: foo
};
 
obj.foo(); // 2

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

function foo() {
  console.log( this.a );
}
 
var obj2 = {
  a: 42,
  foo: foo
};
 
var obj1 = {
  a: 2,
  obj2: obj2
};
 
obj1.obj2.foo(); // 42

显示绑定

通过 call、apply 和 bind 方法进行绑定。

new绑定

function foo(a) {
  this.a = a;
}
 
var bar = new foo(2);
console.log( bar.a ); // 2

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。 

(3)绑定优先级

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

12.JS 执行机制

javascript 是一门单线程语言。

我们一般把 js 中的任务简单的分为同步任务和异步任务,其执行流程如下

前端中高级知识要点总结_第1张图片

上图要表达的内容如下:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue。
  • 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的 Event Loop (事件循环)。

任务还可精细的分为:

macro-task(宏任务):包括整体代码 script,setTimeout,setInterval

micro-task(微任务):Promise,process.nextTick

注:这里的 process.nextTick 相当于 node 版的 setTimeout。

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

前端中高级知识要点总结_第2张图片

我们可以通过以下这个例子来了解其执行机制。

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')
    })
})

第一轮事件循环流程分析:

  • 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 1。
  • 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中。我们暂且记为 setTimeout1。
  • 遇到 process.nextTick(),其回调函数被分发到微任务 Event Queue 中。我们记为 process1。
  • 遇到 Promise,new Promise 直接执行,输出 7。then 被分发到微任务 Event Queue 中。我们记为 then1。
  • 又遇到了 setTimeout,其回调函数被分发到宏任务 Event Queue 中,我们记为 setTimeout2。

然后就有如下队列

前端中高级知识要点总结_第3张图片

  • 上表是第一轮事件循环宏任务结束时各 Event Queue 的情况,此时已经输出了 1 和 7。

  • 我们发现了 process1和 then1 两个微任务。

  • 执行 process1,输出6。

  • 执行 then1,输出8。

第一轮事件循环结束,输出 1,7,6,8。

第二轮时间循环从 setTimeout1 宏任务开始:

  • 首先输出 2。接下来遇到了 process.nextTick(),同样将其分发到微任务 Event Queue 中,记为 process2。new Promise 立即执行输出 4,then 也分发到微任务 Event Queue 中,记为 then2。

前端中高级知识要点总结_第4张图片

  • 第二轮事件循环宏任务结束,我们发现有 process2 和 then2 两个微任务可以执行。
  • 输出 3。 输出 5。
  • 第二轮事件循环结束,第二轮输出 2,4,3,5。

第三轮事件循环开始,此时只剩 setTimeout2 了,执行。

直接输出9。

  • 将 process.nextTick() 分发到微任务 Event Queue 中。记为 process3。
  • 直接执行 new Promise,输出 11。
  • 将 then 分发到微任务 Event Queue 中,记为 then3。

前端中高级知识要点总结_第5张图片

  • 第三轮事件循环宏任务执行结束,执行两个微任务 process3 和 then3。
  • 输出10。
  • 输出12。
  • 第三轮事件循环结束,第三轮输出 9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为 1,7,6,8,2,4,3,5,9,11,10,12。

(请注意,node 环境下的事件监听依赖 libuv 与前端环境不完全相同,输出顺序可能会有误差。我在 node 10.16.0 版本中的输出结果为:1 7 6 8 2 4 9 11 3 10 5 12,这可能是版本差异造成的。)

【参考文章】

这一次,彻底弄懂 JavaScript 执行机制

13.箭头函数与普通函数

箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有以下几点差异:

1、函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。

2、不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

3、不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

4、不可以使用 new 命令,因为:

没有自己的 this,无法调用 call,apply;

没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 __proto__

14.AST

AST,即抽象语法树。介绍可见——AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解

这里引申出一个问题——Babel 是如何把 ES6 转成 ES5 的呢?其实就是:将 ES6 的代码转换为 AST 语法树,然后再将 ES6 AST 转为 ES5 AST,再将 AST 转为代码。

二.JS面向对象

1.模拟new过程

new的执行过程如下:

  1. 创建一个新对象,它继承foo.prototype。目的是继承构造函数原型上的属性和方法;
  2. 执行构造函数,执行时传入相应的参数,同时上下文(this)会被指定为这个新实例。目的是执行构造函数内的赋值操作;
  3. 如果构造函数返回了一个对象,那么这个对象会取代整个new出来的结果。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象。

具体代码如下:

function myNew(func, ...args) {
  const obj = Object.create(func.prototype); // 第一步,创建一个对象
  const res = func.apply(obj, args); // 第二步,执行构造函数
  return typeof res === 'object' ? res : obj // 第三步,返回对象
}

// 测试
function Name(name) {
  this.name = name
}

const res = myNew(Name, 'Tom');
console.log(res)                 // Name {name: "Tom"}
console.log(res instanceof Name) // true 

2.继承(ES5实现)

(1)原型链方式

ECMAScript中将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

// 父类
function SuperType() {
  this.property = true;
}
 
SuperType.prototype.getSuperValue = function() {
  return this.property;
};
 
// 子类
function SubType() {
  this.subproperty = false;
}
 
// 实现继承
SubType.prototype = new SuperType();
 
// 添加新方法(注:需要放在继承之后,不然会被覆盖。当然,也不能以字面量的形式修改原型,否则继承无效)
SubType.prototype.getSubValue = function() {
  return this.subproperty;
};
 
var instance = new SubType();
alert(instance.getSuperValue()); // true

实现继承的本质是重写原型对象,代之一个新类型的实例

原型链的问题:

问题一:引用类型值共享的问题

function SuperType() {
  this.colors = ['red', 'blue', 'green'];
}
 
function SubType() {
}
 
// 继承
SubType.prototype = new SuperType();
 
var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors); // ['red', 'blue', 'green', 'black']
 
var instance2 = new SubType();
alert(instance2.colors); // ['red', 'blue', 'green', 'black']

我们发现在实例instance1中添加成员“black”,结果在实例instance2中也存在,这往往不是我们想要的效果。

问题二:在创建子类型的实例时,不能向超类型的构造函数中传递参数

基于上述两个问题,导致我们在实践中很少会单独使用原型链来实现继承。

(2)借用构造函数

借用构造函数的思想:在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。

function SuperType() {
  this.colors = ['red', 'blue', 'green'];
}
 
function SubType() {
  // 继承
  SuperType.call(this);
}
 
 
var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors); // ['red', 'blue', 'green', 'black']
 
var instance2 = new SubType();
alert(instance2.colors); // ['red', 'blue', 'green']

我们在创建SubType实例时,调用了SuperType构造函数,所以,SubType实例都具有自己的colors属性副本。

传递参数

function SuperType(name) {
  this.name = name;
}
 
function SubType() {
  // 继承
  SuperType.call(this, 'Tom');
 
  // 实例属性
  this.age = 2;
}
 
 
var instance = new SubType();
alert(instance.name); // "Tom"
alert(instance.age);  // 2

借用构造函数的问题

在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。而使用构造函数模式存在一个问题——每个方法都要在实例上重新创建一遍。所以,借用构造函数的技术也是很少单独使用的。

(3)组合继承

组合继承,又叫伪经典继承,是JavaScript中最常用的继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
 
SuperType.prototype.sayName = function() {
  alert(this.name);
};
 
function SubType(name, age) {
  // 继承属性
  SuperType.call(this, name);
 
  this.age = age;
}
 
// 继承方法
SubType.prototype = new SuperType();
 
SubType.prototype.constructor = SubType; // 修改构造器
SubType.prototype.sayAge = function() {
  alert(this.age);
};
 
var instance1 = new SubType('Tom', 3);
instance1.colors.push('black');
alert(instance1.colors); // ['red', 'blue', 'green', 'black']
instance1.sayName(); // "Tom"
instance1.sayAge(); // 3
 
var instance2 = new SubType('Jerry', 2);
alert(instance2.colors); // ['red', 'blue', 'green']
instance2.sayName(); // "Jerry"
instance2.sayAge(); // 2

当然,还有其它几种继承方法:原型式继承、寄生式继承和寄生组合式继承,篇幅有限,这里就不在介绍了。

3.继承(ES6实现)

ES6中的Class可以通过extends关键字实现继承,这比ES5通过修改原型链更容易理解。

class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  
  toString() {
    return this.x
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y) // 需要先调用父类的 constructor(x, y),不然下面的 this 会报错
    this.color = color
  }

  toString() {
    return this.color + ' ' + super.toString()
    // 调用父类的 toString()
  }
}

let cp = new ColorPoint(1, 2, 'red')
console.log(cp instanceof ColorPoint)  // true
console.log(cp instanceof Point)       // ture
console.log(cp.x)                      // 1
console.log(cp.toString())             // red 1

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。

ES5与ES6实现继承的差别:

ES5的继承实质是先创建子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创建父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

三.设计模式

设计模式有很多种,这里只讲几个常见的模式。

在开始了解设计模式前,有必要先了解一下设计原则。

1.设计原则

设计模式的原则有:单一职责原则、开放封闭原则(也称开闭原则)、里氏代换原则、合成复用原则、接口隔离原则和迪米特法则(也称为最小知识原则)。这里主要介绍三种:单一职责原则、开放封闭原则和最小知识原则。

(1)单一职责原则

就一个类而言,应该仅有一个引起它变化的原因。在JavaScript中,由于类使用得并不多,所以这更多体现在对象和方法上。

结论:一个对象或方法只做一件事情

(2)开放封闭原则

开放封闭原则是最重要的一个原则,是指对扩展开放,对修改封闭。这能增加可维护性,避免因为修改给系统带来的不稳定性。

(3)最小知识原则

最少知识原则说的是一个软件实体应当尽可能少地与其他实体发生相互作用。

遵守设计原则的目的是实现高内聚低耦合的代码。但在实际开发中也不必刻板的去遵守这些原则,应根据实际情况灵活应用。

2.单例模式

单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是一种常用的模式,有些对象往往只需要一个,比如线程池、全局缓存 、浏览器中的window对象等。

实现一个单例模式很简单,只需用一个变量来表示当前是否已经创建了某个类的实例,如果是,则在下一次获取该类实例时,返回之前创建的对象。

var Singleton = function(name) {
  this.name = name;
}

Singleton.instance = null;
Singleton.prototype.getName = function() {
  alert(this.name);
}

Singleton.getInstance = function(name) {
  if (!this.instance) {
    this.instance = new Singleton(name);
  }
  return this.instance;
}

var a = Singleton.getInstance('sven1');
var b = Singleton.getInstance('sven2');

alert(a === b); // true

常用单例模式之惰性单例【推荐】

以登录弹框为例

// 将结果缓存起来,这里使用了闭包
var getSingle = function(fn) {
  var result;
  return function() {
    return result || (result = fn.apply(this, arguments)); // 执行fn
  }
};

var createLoginLayer = function() {
  var div = document.createElement('div');
  div.innerHTML = '我是登录浮窗';
  div.style.display = 'none';
  document.body.appendChild(div);
  return div;
};

var createSingleLoginLayer = getSingle(createLoginLayer);

// 使用单例,让创建的结果(div)缓存起来,即createLoginLayer方法只执行了一次
document.getElementById('loginBtn').onclick = function() {
  var loginLayer = createSingleLoginLayer();
  loginLayer.style.display = 'block';
};

上面代码的好处在于创建对象和管理单例的逻辑都分开了,这让我们的代码能更加复用。

下面我们可以在试试创建唯一的iframe用于加载第三方页面:

var createSingleIframe = getSingle(function() {
  var iframe = document.createElement('iframe');
  document.body.appendChild(iframe);
  return iframe;
});

document.getElementById('loginBtn').onclick = function() {
  var loginLayer = createSingleIframe();
  loginLayer.src = 'https://baidu.com';
}

3.工厂模式

工厂模式 (Factory Pattern),根据不同的输入返回不同类的实例,一般用来创建同一类对象。工厂方式的主要思想是将对象的创建与对象的实现分离

工厂模式有多种:简单工厂模式、工厂方法模式、抽象工厂模式,这里介绍简单工厂模式。

function createFood(menu) {
  switch(menu) {
    case '回锅肉': return new Food1();
    case '红烧肉': return new Food2();
    default: throw new Error('此菜本店没!')
  }
}

function Food1() {
  this.name = '回锅肉'
}

function Food2() {
  this.name = '红烧肉'
}

var food1 = createFood('回锅肉'); // Food1 {name: "回锅肉"}
var food2 = createFood('粉蒸肉'); // Error: 此菜本店没!

4.策略模式

策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

策略模式也是比较常用的模式,常用来改写 switch 语句。

下面例子使用策略模式实现计算奖金

// S, A, B 代表绩效等级
// salary 代表月薪

// 定义策略对象,封装算法
var strategies = {
  'S': function(salary) {
    return salary * 4;
  },
  'A': function(salary) {
    return salary * 3;
  },
  'B': function(salary) {
    return salary * 2;
  }
}

// Context
var calculateBonus = function(level, salary) {
  return strategies[level](salary)
}

console.log(calculateBonus('S', 20000)); // 80000
console.log(calculateBonus('A', 10000)); // 30000

策略模式的优点:

  • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对开放封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩散。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  • 在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案。 

策略模式的缺点:

  • 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy。此时strategy要向客户暴露它的所有实现,这是违反最少知识原则的。

5.代理模式

代理模式又称委托模式,它为目标对象创造了一个代理对象,以控制对目标对象的访问。

JavaScript中常用的代理模式有虚拟代理和缓存代理。

(1)虚拟代理

虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。

例子:用虚拟代理实现图片预加载

// 目标对象
var myImage = (function() {
  var imgNode = document.createElement('img');
  document.body.appendChild(imgNode);

  return {
    setSrc: function(src) {
      imgNode.src = src;
    }
  }
})();

// 代理对象
var proxyImage = (function() {
  var img = new Image;
  // 图片加载完成后再替换
  img.onload = function() {
    myImage.setSrc(this.src);
  }

  return {
    setSrc: function(src) {
      myImage.setSrc('./loading.gif'); // 先设置loading图占位
      img.src = src;
    }
  }
})();

proxyImage.setSrc('https://img1.sycdn.imooc.com/5d2446e9000175aa06400359.jpg');

当然,不使用代理模式也能实现上述效果,使用的好处在于更加符合单一职责原则,开闭原则。

(2)缓存代理

缓存代理可以为一些开销很大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果。

例子:假如计算乘积是一个复杂的计算

// 计算方法
var mult = function() {
  console.log('计算乘积');
  var a = 1;
  for(var i = 0, len = arguments.length; i < len; i++) {
    a = a * arguments[i];
  }
  return a;
}

var proxyMult = (function() {
  var cache = {};
  return function() {
    var args = Array.prototype.join.call(arguments,  ','); // 将传递的参数拼接为逗号分隔的字符串
    if (args in cache) {
      return cache[args]; // 返回缓存中的值
    }
    return cache[args] = mult.apply(this, arguments); // 返回值并存入缓存
  }
})();

proxyMult(1,2,3,4); // 24
proxyMult(1,2,3,4); // 24   此次并未计算,而是从缓存中获取

6.发布订阅模式

发布订阅模式也叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。DOM 事件就是发布订阅模式。再比如 Vue 中视图与数据的双向绑定也是发布订阅模式。

这里以购房者去售楼处买房为例。购房者在售楼处留下电话,当新楼盘开售后,售楼处就会给购房者发短信通知。

售楼处充当发布者,购房者充当订阅者。

var salesOffices = {}; // 定义售楼处

salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数

salesOffices.listen = function(fn) { // 增加订阅者
  this.clientList.push(fn); // 订阅的消息添加进缓存列表
};

salesOffices.trigger = function() { // 发布消息
  for (var i = 0, fn; fn = this.clientList[i++];) {
    fn.apply(this, arguments); // arguments 是发布消息时带上的参数
  }
};

// 测试
salesOffices.listen(function(price, squareMeter) { // 小明订阅消息
  console.log('价格 ' + price);
  console.log('面积 ' + squareMeter);
});

salesOffices.listen(function(price, squareMeter) { // 小红订阅消息
  console.log('价格 ' + price);
  console.log('面积 ' + squareMeter);
});

salesOffices.trigger(2000000, 88);
// 价格 2000000
// 面积 88
// 价格 2000000
// 面积 88
salesOffices.trigger(3000000, 100);
// 价格 3000000
// 面积 100
// 价格 3000000
// 面积 100

不难看出,这个例子还有很多问题。比如,这里每个订阅者都会接受到所有消息,但小明只买88平米的房子,其他消息就没有必要发送了。

下面 ,看看一个通用的发布订阅模式。

var event = {
  clientList: {},
  listen: function(key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn); // 订阅消息添加进缓存
  },
  trigger: function() {
    var key = Array.prototype.shift.call(arguments), // 取得第一个参数
        fns = this.clientList[key];

    if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
      return false;
    }

    for (var i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments); // arguments 是执行 trigger 时的参数
    }
  }
};

// 给对象安装发布订阅模式的方法
var installEvent = function(obj) {
  for (var i in event) {
    obj[i] = event[i];
  }
};

// 测试
var salesOffices = {};
installEvent(salesOffices);

salesOffices.listen('squareMeter88', function(price) { // 小明订阅
  console.log('价格 ' + price);
});

salesOffices.listen('squareMeter100', function(price) { // 小红订阅
  console.log('价格 ' + price);
});

salesOffices.trigger('squareMeter88', 2000000); // 价格 2000000
salesOffices.trigger('squareMeter100', 3000000); // 价格 3000000

7.装饰者模式

装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责

以飞机大战为例,飞机在升级过程中会发射不同子弹。

var plane = {
  fire: function(){
    console.log( '发射普通子弹' );
  }
}
var missileDecorator = function(){
  console.log( '发射导弹' );
}
var atomDecorator = function(){
  console.log( '发射原子弹' );
}

var fire1 = plane.fire;
plane.fire = function(){
  fire1();
  missileDecorator();
}

var fire2 = plane.fire;
plane.fire = function(){
  fire2();
  atomDecorator();
}

plane.fire(); // 分别输出:发射普通子弹、发射导弹、发射原子弹

再看个实际例子,比如我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,我们一般都会先保存好原先的 window.onload,把它放入新的 window.onload 里执行:

window.onload = function(){
  console.log(1);
}

var _onload = window.onload || function(){};
window.onload = function(){
  _onload();
  console.log(2);
} 

这样,代码就符合了开闭原则。 

四.算法

 这里主要介绍排序算法

前端中高级知识要点总结_第6张图片

1.冒泡排序

function bubbleSort (arr) {
  // 冒泡排序
  for (let i = arr.length - 1, tmp; i > 0; i--) {
    for (let j = 0; j < i; j++) {
      // 由小到大,升序排序
      if (arr[j] > arr[j + 1]) {
        tmp = arr[j]
        arr[j] = arr[j + 1]
        arr[j + 1] = tmp
      }
    }
  }
  return arr
}
 
let arr = [3, 2, 4, 9, 1, 5, 7, 6, 8]
let arrSorted = bubbleSort(arr)
console.log(arrSorted)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

2.优化冒泡排序

function bubbleSort (arr) {
  // 冒泡排序
  for (let i = arr.length - 1, tmp; i > 0; i--) {
    let flag = true
    for (let j = 0; j < i; j++) {
      // 由小到大,升序排序
      if (arr[j] > arr[j + 1]) {
        flag = false
        tmp = arr[j]
        arr[j] = arr[j + 1]
        arr[j + 1] = tmp
      }
    }
    if (flag) break; // 没交换元素,排序完成
  }
  return arr
}
 
let arr = [3, 2, 4, 9, 1, 5, 7, 6, 8]
let arrSorted = bubbleSort(arr)
console.log(arrSorted)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

3.选择排序

function SelectionSort(arr) {
  // 选择排序,每次选择最小值
  for (let i = 0; i < arr.length - 1; i++) {
    let minIndex = i
    for (let j = i + 1; j < arr.length; j++) {
      minIndex = arr[j] < arr[minIndex] ? j : minIndex
    }
    // 由小到大排序,把最小值放在前面
    let temp = arr[i]
    arr[i] = arr[minIndex]
    arr[minIndex] = temp
  }
  return arr
}
 
let arr = [3, 2, 4, 9, 1, 5, 7, 6, 8]
let arrSorted = SelectionSort(arr)
console.log(arrSorted)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

// 待完善... 

五.HTTP 网络/通信

1.GET 和 POST 的区别

  • GET 在浏览器回退时是无害的,而 POST 会再次提交请求
  • GET 产生的 URL 地址可以被收藏,而 POST 不可以
  • GET 请求会被浏览器主动缓存,而 POST 不会,除非手动设置
  • GET 请求只能进行 url 编码,而 POST 支 持多种编码方式
  • GET 请求参数会被完整保留在浏览器历史记录里,而 POST 中的参数不会被保留
  • GET 请求在 URL 中传送的参数是有长度限制的,而 POST 没有限制对参数的数据类型
  • GET 只接受 ASCII 字符,而 POST 没有限制
  • GET 比 POST 更不安全,因为参数直接暴露在 URL 上,所以不能用来传递敏感信息
  • GET 参数通过 URL 传递,POST 放在 Request body 中

2.HTTP方法

  • GET  获取资源
  • POST 传输资源
  • PUT  更新资源
  • DELETE 删除资源
  • HEAD  获得报文首部

3.HTTP状态码

  • 1xx:指示信息-表示请求已接收,继续处理
  • 2xx:成功-表示请求已被成功接收
  • 3xx:重定向-要完成请求必须迸行更迸一步的操作
  • 4xx:客戸端错误-请求有语法错误或请求无法实现
  • 5xx:服各器错误-服务器未能实现合法的请求

常见状态码

  • 200 OK:客户端请求成功
  • 206 Partial Content:客户发送了一个带有Range头的GET请求,服务器完成了它
  • 301 Moved Permanently:所请求的页面已经转移至新的url
  • 302 Found:所请求的页面已经临时转移至新的url
  • 304 Not Modified:客户端有缓冲的文档并发出了一个条件性的请求,服务器告诉客户,原来缓冲的文档还可以继续使用
  • 400 Bad Request:客户端请求有语法错误,不能被服务器所理解
  • 401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden:对被请求页面的访问被禁止
  • 404 Not Found:请求资源不存在
  • 500 Internal Server Error:服务器发生不可预期的错误,原来缓冲的文档还可以继续使用
  • 503 Server Unavailable:请求未完成,服务器临时过载或当机,一段时间后可能恢复正常

4.前后端如何通信

  • Ajax
  • WebSocket
  • CORS

Ajax

Ajax 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。

Ajax 常见应用:运用 XMLHttpRequest 或新的 Fetch API 与网页服务器进行异步资料交换。

这里我们手写一个简单的 XHR 请求过程

function request() {
  // 1.获取一个 XHR 实例
  const xhr = new XMLHttpRequest()
  
  // 2.初始化请求
  xhr.open('POST', 'http://192.168.3.195:8088/setUsername', true)
  
  // 3.事件处理器 在每次 readyState 属性变化时被自动调用
  xhr.onreadystatechange = function() {
    // 请求完成且状态为 200
    if (xhr.readyState == 4 && xhr.status == 200) {
      // 成功
      // 得到服务端返回的数据
      let res = JSON.parse(xhr.responseText)
    } else {
      // 其他情况
    }
  }
  
  // 设置请求头
  xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8')
  xhr.setRequestHeader('Accept', 'application/json, text/plain, */*')

  // 允许跨域
  xhr.withCredentials = true
  
  // 设置发送的数据
  const requestData = JSON.stringify({
    username: 'Tom'
  })
  
  // 4.发送请求
  xhr.send(requestData)
}

WebSocket

是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

常见应用:客服系统、物联网数据传输系统等。

推荐教程:WebSocket 教程 - 阮一峰

CORS

CORS 全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了AJAX只能同源使用的限制。

5.跨域通信及其方式

跨域通信

协议、IP和端口只要其中之一不同就算跨域。

跨域通信方式

  • Jsonp
  • Hash
  • postMessage
  • WebSocket
  • CORS

6.HTTPS

HTTPS 是在 HTTP 上建立 SSL 加密层,并对传输数据进行加密,是 HTTP 协议的安全版。

HTTPS 主要作用是:

(1)对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;

(2)对网站服务器进行真实身份认证。

HTTPS 与 HTTP 的区别:

  • HTTP 是明文传输协议,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全;
  • HTTPS 比 HTTP 更加安全,对搜索引擎更友好,利于 SEO,谷歌、百度优先索引 HTTPS 网页;
  • HTTPS 需要用到 SSL 证书,而 HTTP 不用;
  • HTTPS 标准端口 443,HTTP 标准端口 80;
  • HTTPS 基于传输层,HTTP 基于应用层;
  • HTTPS 在浏览器显示绿色安全锁,HTTP 没有显示。

参考文章:深入理解HTTPS工作原理

六.前端安全

1.CSRF,跨站请求伪造

定义

CSRF即Cross-site request forgery(跨站请求伪造),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

防御

  • 添加校验token。系统开发人员可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务端进行token校验,如果请求中没有token或者token内容不正确,则认为是CSRF攻击而拒绝该请求。
  • 尽量使用POST,限制GET。POST请求有一定作用,但并不一定完全安全。
  • 检查Referer字段。通过验证请求头的Referer来验证来源站点,但请求头很容易伪造。

2.XSS,跨域脚本攻击

定义

跨站脚本攻击是指恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。

防御

  • 输入检查:对输入内容中的

你可能感兴趣的:(前端面试,面试,css,javascript)