前端基础总结(1)

152 阅读6分钟

十个问题做一个对前端知识的总结和梳理。

1. 什么是闭包

一言以蔽之:闭包就是能从外部读取函数内部变量的函数。Javascript语言中,只有函数内部的子函数能够读取局部变量,所以可以把闭包简单理解为 “定义在一个函数内部的函数”。

function f1(){
  let name = "Steve";
  function hello(){
    console.log("Hello " + name);
  }
  
  changeName = function(){
    name = "Frank";
    console.log("Sorry, it's Frank");
    }
  return hello;
}

let greeting = f1();
greeting();  // "Hello Steve"
changeName();   // "Sorry, it's Frank"

上述代码通过在函数f1内部定义子函数hello,在子函数内访问父函数的变量,父函数返回子函数对象的方法,实现了从外部读取函数的内部变量。这里hello函数就是闭包

闭包的用途

  1. 可以读取函数内部的变量
  2. 让这些变量的值始终保存在内存中: 观察上述代码,greeting被调用之后,再调用changeName,函数f1内的局部变量name被改成了"Frank",说明函数f1内部的数据并没有在函数被调用后被垃圾回收机制清除。
  3. 用闭包模仿私有方法:通过使用闭包,JS语言可以模仿Java实现私有方法。代码如下:
var counter = function(){
  var privateCounter = 0;
  function changeBy(val){
    privateCounter += val;
  }
  
  return {
    increment: function(){
      changeBy(1);
    },
    
    decrement: function(){
      changeBy(-1);
    },
    
    value: function(){
      return privateCounter;
    },
  }
};

var counter1 = counter();
var counter2 = counter();

alert(counter1.value());  // 0.

counter1.increment();
counter1.increment();
alert(counter1.value()); // 2.

counter1.decrement();
alert(counter1.value()); // 1.
alert(counter2.value()); // 0.

需要注意的是:

  1. 函数function()的词法作用域被三个函数共享。该函数的局部变量privateCounterchangeBy函数无法从外部访问,只能通过返回的三个方法访问,从而实现了“私有”特性。
  2. 通过声明不同的变量,引用counter函数,变量的词法作用域相互独立。

闭包的缺点

闭包最大的缺点在于它非常影响性能——拖慢处理速度且占用很多内存,在旧版IE浏览器中还可能导致内存泄漏。所以如果没有特殊的需求,请慎用闭包。
缺点示例请看MDN文档:创建对象时该如何正确地定义对象方法

2. call、bind、apply的用法分别是什么?

call方法

call()方法使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。语法为:

function.call(thisArg, arg1, arg2, ...)

thisArg参数是可选的,this可能不是该方法看到的实际值;在非严格模式下,这个函数的thisArg参数指定为nullundefined时会自动替换为指向全局对象,原始值会被包装。
用法示例:

  1. 使用call方法调用父构造函数
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);
  1. 使用call方法调用匿名函数
var animals = [
  { species: 'Lion', name: 'King' },
  { species: 'Whale', name: 'Fail' }
];

for (var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}
  1. 使用call方法调用函数并且指定
function greet() {
  var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
  console.log(reply);
}

var obj = {
  animal: 'cats', sleepDuration: '12 and 16 hours'
};

greet.call(obj);  // cats typically sleep between 12 and 16 hours
  1. thisArg参数不被指定时,this值自动绑定为全局对象
var randomNum = 1234567;
var randomString = "hello"

function display() {
  console.log('sData value is %s ', this.randomNum);
  console.log('sData value is %s ', this.randomString);
}

display.call();  // sData value is 1234567\nsData value is hello

bind方法

bind()方法可以理解为:在调用一个函数的时候,绑定这个函数的this指向。请看如下代码

this.x = 9;  // 浏览器中的全局作用域里this指向全局的window对象
let module = {
  x: 81;
  getX: function(){ return this.x; }
}

module.getX();  // 81

let retrieveX = module.getX;
retrieveX();  // 返回9,因为函数是在全局作用域中调用的

let boundGetX = retrieveX.bind(module);
boundGetX();  // 返回81,因为指定了调用retrieve方法时的this为module对象

apply方法

apply()方法调用一个具有给定this值的函数,以及一个以数组形式提供的参数。语法为:

func.apply(thisArg, [argsArray])

该方法其实与call()方法作用相似。只是参数argsArray要以数组形式进行传递。
用法示例:

  1. 向原有数组追加数据
let array = [1, 2, 3];
let string = ["hello", "hi"];
array.push.apply(array, string);
console.log(array);  // [1, 2, 3, "hello", "hi"]
  1. apply避免循环
/* 找出数组中最大/小的数字 */
var numbers = [5, 6, 2, 3, 7];

/* 使用Math.min/Math.max以及apply 函数时的代码 */
var max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);

但这种方式会有超出Javascript引擎参数长度上限的风险。风险导致的直接后果就是报错。所以如果数组长度很大,可以尝试切块再用apply方法。

三种方法比较

一个例子作比较:

var obj = { x: 81; }
var foo = { getX: function() { return this.x; }

console.log(foo.getX.call(obj));
console.log(foo.getX.bind(obj)());
console.log(foo.getX.apply(obj));

三个输出都是81,但bind()方法后面多了对括号。这说明:如果想回调执行,建议用bind()方法,apply()/call()方法则会立即执行函数。

3. HTTP响应状态码

HTTP响应状态码用来表明特定HTTP请求是否完成,分为五大类:

  1. 信息相应(100-199)
  2. 成功相应(200-299)
  3. 重定向响应(300-399)
  4. 客户端错误响应(400-499)
  5. 服务端错误相应(500-599)

状态码列举

  • 100, continue—— 这个临时响应表明,迄今为止的所有内容都是可行的,客户端应该继续请求,如果已经完成,则忽略它。
  • 102, processing——此代码表示服务器已收到并正在处理该请求,但当前没有响应可用。
  • 201, created——该请求已成功,并因此创建了一个新的资源。这通常是在POST请求,或是某些PUT请求之后返回的响应。
  • 202, accepted——请求已经接收到,但还未响应,没有结果。意味着不会有一个异步的响应去表明当前请求的结果,预期另外的进程和服务去处理请求,或者批处理。
  • 204, no content——对于该请求没有的内容可发送,但头部字段可能有用。用户代理可能会用此时请求头部信息来更新原来资源的头部缓存字段。
  • 301, moved permanently——请求资源的URL已永久更改。在响应中给出了新的URL。
  • 302, found——此响应代码表示所请求资源的URI已 暂时 更改。未来可能会对URI进行进一步的改变。因此,客户机应该在将来的请求中使用这个相同的URI。
  • 303, see other——服务器发送此响应,以指示客户端通过一个GET请求在另一个URI中获取所请求的资源。
  • 403, forbidden——客户端没有访问内容的权限;也就是说,它是未经授权的,因此服务器拒绝提供请求的资源。与401 Unauthorized不同,服务器知道客户端的身份。
  • 404, not found——服务器找不到请求的资源。在浏览器中,这意味着无法识别URL。在API中,这也可能意味着端点有效,但资源本身不存在。服务器也可以发送此响应,而不是403 Forbidden,以向未经授权的客户端隐藏资源的存在。这个响应代码可能是最广为人知的,因为它经常出现在网络上。
  • 500, internal server error——服务器遇到了不知道如何处理的情况。
  • 501, not implemented——服务器不支持请求方法,因此无法处理。服务器需要支持的唯二方法(因此不能返回此代码)是 GET and HEAD.
  • 502, bad gateway——此错误响应表明服务器作为网关需要得到一个处理这个请求的响应,但是得到一个错误的响应。

4. 如何实现数组去重?

问题:假设有数组 array = [1,5,2,3,4,2,3,1,3,4]
你要写一个函数 unique,使得
unique(array) 的值为 [1,5,2,3,4]
也就是把重复的值都去掉,只保留不重复的值。

要求写出两个答案:

  1. 一个答案不使用 Set 实现(6分)
  2. 另一个答案使用 Set (4分)
  3. (附加分)使用了 Map / WeakMap 以支持对象去重的,额外加 5 分。
  4. 说出每个方案缺点的,再额外每个方案加 2 分。 回答:
    不使用Set实现
let uniqueWithoutSet = function(array){
    let uniqueArray = [];
    for(let i = 0; i < array.length; i++){
        if(uniqueArray.indexOf(array[i]) === -1){
            uniqueArray.push(array[i]);
        }
    }
    return uniqueArray;
}
/* 缺点:Nan, {}空对象 无法去重 */

使用Set实现

let uniqueWithSet = function(array){
  return [...new Set(array)];
}
/* 该方法无法去重空对象{} */

使用Map数据结构实现

let uniqueWithMap = function(array){
    const map = new Map();
    const arr = new Array();
    for(let i = 0; i < array.length; i++){
        if(map.has(array[i])){
            map.set(array[i], true);
        } else{
            map.set(array[i], false);
            arr.push(array[i]);
        }
    }
    return arr;
}
/* 还是无法去重空对象 */

5. DOM事件

什么是事件委托?

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这样做可以节省内存,同时也可以监听动态元素。

如何阻止默认动作

使用preventDefault()方法

let $a = document.querySelector("#a")[0];
$a.onclick = function(e){
  alert("默认事件被阻止了");
  e.preventDefault();
}

如何阻止事件冒泡

使用stopPropagation()方法

function stopBubble(e){
  if(e && e.stopPropagation){  // Non-IE
    e.stopPropagation();
  } else{
    // IE
    window.event.cancelBubble = true;
  }
}

6. 如何理解JS的继承?

JavaScript是一门动态语言。JavaScript的继承是基于原型链实现的,尽管从ES5之后引入了class关键字,但这也只是语法糖,本质上还是基于原型。
谈到JavaScript的继承时,只涉及到一种结构:对象。每个实例对象(object)都有一个私有属性(__proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的私有属性(__proto__)指向它的构造函数的原型对象,层层向上直到一个对象的原型对象为null——原型链中的最后一个环节。
几乎所有JavaScript的对象都是位于原型链顶端的Object的实例。
当试图访问一个对象的属性时,不仅会从对象自身搜寻,还会从对象的原型,以及原型的原型,层层向上搜寻,直到找到一个名字匹配的属性或到达原型链的末尾。
直接上代码

var o = {
  a: 2,
  m: function() {
    return this.a + 1;
  }
};

console.log(o.m());  // 3
var p = Object.create(o);  // p是一个继承自o的对象
p.a = 4;  // 创建p的自身属性a
console.log(p.m());  // 5

使用不同的方法创建对象和生成原型链

var o = { a: 1 };
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];
// a ---> Array.prototype ---> Object.prototype ---> null

function f() { return 2; }
// f ---> Function.prototype ---> Object.prototype ---> null

/* 使用构造器创建对象 */
function Graph(){
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
}

var g = new Graph();  // g是生成的对象,自身属性有“vertices”和“edges”
// g被实例化时,g.__proto__ === Graph.prototype

/* 使用Object.create创建的对象*/
var a = { a: 1 };  
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a);  // 1 (继承于a)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
console.log(c.a);  // 1 (同样继承于a)

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty);  // undefined,因为d没有继承Object.prototype

使用class关键字创建对象(颇有Java风格,但本质是语法糖,还是基于原型链实现)

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

在编写基于原型链的复杂代码时,要关注性能问题,因为在原型链上查找比较耗时。当查找对象不存在的属性时,层层向上遍历,最终返回undefined。一旦原型链过长,就会造成不必要的性能开销。
要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,必须使用所有对象从Object.prototype继承的hasOwnProperty方法。

7. 数组排序

实现一个插入排序

function insertionSort(arr){
    if(arr.length === 0 || arr.length === 1){ return arr; }

    let length = arr.length;
    for(let i = 1; i<length; i++){
        let temp = arr[i];
        let j = i;
        for(; j>0; j--){
            if(temp >= arr[j-1]){
                break;
            }
            arr[j] = arr[j-1];
        }
        arr[j] = temp;
    }
    return arr;
}

8. 谈谈你对Promise的理解

Promise的用途

Promise对象用于表示一个异步操作的最终完成(或失败)及其结果值。它能够把异步操作最终的成功返回值或失败原因和相应的处理程序(回调)关联起来。

创建一个new Promise

语法:

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

9. 说说跨域

同源

通过window.originlocation.origin可以得到当前源。 源 = 协议 + 域名 + 端口号
如果两个url的协议、域名、端口号完全一致,则这两个url就是同源的。例如https://baidu.comhttps://www.baidu.com不同源,只有完全一致才算同源。

跨域

如果JS在源A,获取了源B的数据,该操作叫做跨域。根据“同源策略”:浏览器规定是不允许跨域的,即不同源的页面之间,不准互相访问数据。

JSONP跨域

如果浏览器是旧版IE浏览器,不支持CORS跨域,则需要使用这种方法实现跨域。具体思路是:JS脚本可以被随意引用,所以只需要让JS脚本包含目标数据即可。

CORS跨域

chrome,Mozilla浏览器支持CORS跨域,只需在headers添加一句Access-Control-Allow-Origin: http://your_url.example即可。

10. 谈谈你对前端的理解

前端是一门艺术。后端的工作主要是与数据和命令行打交道,需要更深的逻辑和更高难度的技术实力。前端则是更多地负责设计一个用户友好的web页面,因此比较考验页面的布局设计能力和审美的高低。因此前端不需要后端那般复杂缜密的逻辑以及需要时间和项目经验积累出来的深厚技术功底。
前端搭建起用户与数据的桥梁。我们逛淘宝时,看到的商品卡片、购物车里的商品列表、退货退款记录等等,其实都是躺在后端数据库里的一条条数据。我们的每一笔下单、每一次浏览、每一条搜索等等,都会在后台数据库被记录下来。如果没有技术背景和用户友好的web页面,我们就很难实现上述的场景。所以前端负责实现这些需求,提供一种直观友好的交互界面,搭建起用户与数据的桥梁。此外,前端还负责哪些数据不可以向零售用户展示(比如GMV数据),防范数据泄露的风险。
前端不仅仅是设计网页。牛逼的前端不仅仅是考虑设计网页。做到更高的程度,还会考虑如何设计更牛逼的浏览器内核,更优秀稳健的框架,更炫酷的数据可视化界面等等。当涉及到这种底层的领域时,需要的技术栈不会再是相对简单的HTML+CSS+JavaScript三连,而是成体系的计算机科学知识体系(计组、算法与数据结构、计算机网络、操作系统、数据库、图形学等等),而且还要学习其他的高级语言,如C++、Rust等。所以前端的学问不比后端少。