前端面试纪要

313 阅读17分钟

关于本文

  • 万物皆对象。
  • 前端缘起于JavaScript。
  • 作为一个有灵魂的前端,那么就必须精通JavaScript。

咳咳——废话少说,直接上干货

数据类型

ECMAScript 迄今为止标准定义了 6 种数据类型:

Symbol类型暂时不提,目前使用的并不多

  • 5 种基本数据类型-- StringNumberBooleanUndefinedNull
  • 1 种引用类型-- Object

typeof运算符:用来检测给定变量的数据类型。

  • typeof 运算符返回一个用来表示表达式的数据类型的字符串。
  • typeof 一般只能返回如下几个结果:numberstringbooleanobjectfunctionundefined
var message = "hello";
console.log(typeof(message));//string
console.log(typeof(100));//number

instanceof来检测某个对象是不是另一个对象的实例。

instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

  • 语法:object instanceof constructor
  • 参数:object(要检测的对象.)constructor(某个构造函数)
  • 描述:instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
var a=new Array();
alert(a instanceof Array); // true,
alert(a instanceof Object) //也会返回 true;
//这是因为 Array 是 object 的子类。

function test(){};
var a=new test();
alert(a instanceof test) 会返回true
alert(a==b);  //flase

undefined

变量被创建后,未给该变量赋值,该类型只有一个取值:undefined

var a; // undefined

null

变量或内容值为空(null),可以通过给一个变量赋 null 值来清除变量的内容

var foo = null;

boolean

表示 true 或 false 这两种状态

var suc = true;
var los = false;

number

包括整数和浮点数(包含小数点的数或科学记数法的数)

  • NaN,(非数值,Not-a-Number)。是一个特殊的Number类型,用来表示一个本来要返回数值的操作未返回数值的情况
    • 任何涉及 NaN 的操作都返回 NaN
    • NaN 与任何值都不相等,包括 NaN 本身
40 / NaN  // NaN
NaN === NaN // false

isNaN() 函数用于检查其参数是否是非数字值。

isNaN(123)); // true
isNaN("Hello"); //false

string

使用双引号 " 或单引号 ' 括起来的一个或多个字符 类型转换:

  • toString()
var num = 2;
num.toString() // '2'
var found = true;
found.toString() // 'true'

除去 null 与 undefined 之外,其余数据类型都存在 toString 方法,使用会报错

String()
var sym = Symbol(1);
String(sym) // 'Symbol'
var num = 10;
String(num)  // '10'
var str;
String(str) // 'undefined'

如果值有 toString 方法,则调用该方法并返回响应的结果 如果是 null 返回 'null'

如果是 undefined 返回 'undefined'

  • 隐式转换
var a = 3;
a + ''  // '3'
var obj = {a: 3};
obj + '' // [object object]

转换规则和 String 方法一致

object

对象就是一组数据和功能的集合。

var obj = new Object();
    obj.name = 'zs';
    obj.sayHi = function () { 
      console.log('Hi');
    }
console.log(obj.hasOwnProperty('a')); // 实例对象 true
console.log(obj.hasOwnProperty('sayHi')); // 实例对象 true
console.log(obj); // 实例对象
console.log(obj.constructor); // 构造函数Object()
console.log(obj.__proto__); // 原型
/*
constructor: ƒ Object() // 实例的构造函数
hasOwnProperty: ƒ hasOwnProperty()  // 检测属性在实例对象中,不再原型中
isPrototypeOf: ƒ isPrototypeOf() //用于检查传入的对象是否是另一个对象的原型
*/

this

在 JavaScript 中,研究 this 一般都是 this 的指向问题,核心就是 this 永远指向最终调用它的那个对象,除非改变 this 指向或者箭头函数那种特殊情况

function test() {
    console.log(this);
}

test() // window

var obj = {
  foo: function () { console.log(this.bar) },
  bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo() // 1
foo() // 2
// 函数调用的环境不同,所得到的结果也是不一样的

apply()、call()和 bind() 是做什么的,它们有什么区别

  • 相同点:三者都可以改变 this 的指向

  • 不同点:

apply 方法传入两个参数:一个是作为函数上下文的对象,另外一个是作为函数参数所组成的数组

var obj = {
    name : 'sss'
}

function func(firstName, lastName){
    console.log(firstName + ' ' + this.name + ' ' + lastName);
}

func.apply(obj, ['A', 'B']);    // A sss B

call 方法第一个参数也是作为函数上下文的对象,但是后面传入的是一个参数列表,而不是单个数组

var obj = {
    name: 'sss'
}

function func(firstName, lastName) {
    console.log(firstName + ' ' + this.name + ' ' + lastName);
}

func.call(obj, 'C', 'D');       // C sss D

bind 接受的参数有两部分,第一个参数是是作为函数上下文的对象,第二部分参数是个列表,可以接受多个参数

var obj = {
    name: 'sss'
}

function func() {
    console.log(this.name);
}

var func1 = func.bind(null, 'xixi');
func1();

apply、call 方法都会使函数立即执行,因此它们也可以用来调用函数

bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。而原函数 func 中的 this 并没有被改变,依旧指向全局对象 window

bind 在传递参数的时候会将自己带过去的参数排在原函数参数之前

function func(a, b, c) {
    console.log(a, b, c);
}
var func1 = func.bind(this, 'xixi');
func1(1,2) // xixi 1 2

闭包

简单来说,闭包就是能够读取其他函数内部变量的函数

function Person() {
    var name = 'hello'
    function say () {
        console.log(name)
    }
    return say()
}
Person() // hello

由于 JavaScript 特殊的作用域,函数外部无法直接读取内部的变量,内部可以直接读取外部的变量,从而就产生了闭包的概念

用途:

最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中

注意点:

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露

原型与原型链

原型

JavaScript 是基于原型的

我们创建的每个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

简单来说,就是当我们创建一个函数的时候,系统就会自动分配一个 prototype属性,可以用来存储可以让所有实例共享的属性和方法

用一张图来表示就更加清晰了:

图解:

每一个构造函数都拥有一个 prototype 属性,这个属性指向一个对象,也就是原型对象 原型对象默认拥有一个 constructor 属性,指向指向它的那个构造函数 每个对象都拥有一个隐藏的属性 proto,指向它的原型对象

function Person(){}

var p = new Person();

p.__proto__ === Person.prototype // true

Person.prototype.constructor === Person // true

那么,原型对象都有哪些特点呢

原型特点

function Person(){}
Person.prototype.name = 'tt';
Person.prototype.age = 18;
Person.prototype.sayHi = function() {
    alert('Hi');
}
var person1 = new Person();
var person2 = new Person();
person1.name = 'oo';
person1.name // oo
person1.age // 18
perosn1.sayHi() // Hi
person2.age // 18
person2.sayHi() // Hi

从这段代码我们不难看出:

实例可以共享原型上面的属性和方法 实例自身的属性会屏蔽原型上面的同名属性,实例上面没有的属性会去原型上面找 既然原型也是对象,那我们可不可以重写这个对象呢?答案是肯定的

function Person() {}
Person.prototype = {
    name: 'tt',
    age: 18,
    sayHi() {
        console.log('Hi');
    }
}

var p = new Person()

只是当我们在重写原型链的时候需要注意以下的问题:

function Person(){}
var p = new Person();
Person.prototype = {
    name: 'tt',
    age: 18
}

Person.prototype.constructor === Person // false
p.name // undefined

一图胜过千言万语

在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系 重写原型对象,会导致原型对象的 constructor 属性指向 Object ,导致原型链关系混乱,所以我们应该在重写原型对象的时候指定 constructor( instanceof 仍然会返回正确的值) Person.prototype = { constructor: Person } 注意:以这种方式重设 constructor 属性会导致它的 Enumerable 特性被设置成 true(默认为false)

既然现在我们知道了什么是 prototype(原型)以及它的特点,那么原型链又是什么呢?

原型链 JavaScript 中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链

同样的,我们使用一张图来描述

所有原型链的终点都是 Object 函数的 prototype 属性

Objec.prototype 指向的原型对象同样拥有原型,不过它的原型是 null ,而 null 则没有原型

清楚了原型链的概念,我们就能更清楚地知道属性的查找规则,比如前面的 person 实例属性.如果自身和原型链上都不存在这个属性,那么属性最终的值就是 undefined ,如果是方法就会抛出错误

class类 ES6 提供了 Class(类) 这个概念,作为对象的模板,通过 class 关键字,可以定义类

为什么会提到 class :

ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已

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

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

// 可以这么改写
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

class 里面定义的方法,其实都是定义在构造函数的原型上面实现实例共享,属性定义在构造函数中,所以 ES6 中的类完全可以看作构造函数的另一种写法

除去 class 类中的一些行为可能与 ES5 存在一些不同,本质上都是通过原型、原型链去定义方法、实现共享。所以,还是文章开始那句话 JavaScript是基于原型的

更多 class 问题,参考这里

关系判断 instanceof

最常用的确定原型指向关系的关键字,检测的是原型,但是只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型

function Person(){}
var p = new Person();

p instanceof Person // true
p instanceof Object // true
hasOwnProperty

通过使用 hasOwnProperty 可以确定访问的属性是来自于实例还是原型对象

function Person() {}
Person.prototype = {
    name: 'tt'
}
var p = new Person();
p.age = 15;

p.hasOwnProperty('age') // true
p.hasOwnProperty('name') // false

原型链的问题 由于原型链的存在,我们可以让很多实例去共享原型上面的方法和属性,方便了我们的很多操作。但是原型链并非是十分完美的

function Person(){}
Person.prototype.arr = [1, 2, 3, 4];

var person1 = new Person();
var person2 = new Person();

person1.arr.push(5) 
person2.arr // [1, 2, 3, 4, 5]

引用类型,变量保存的就是一个内存中的一个指针。所以,当原型上面的属性是一个引用类型的值时,我们通过其中某一个实例对原型属性的更改,结果会反映在所有实例上面,这也是原型 共享 属性造成的最大问题

另一个问题就是我们在创建子类型(比如上面的 p)时,没有办法向超类型( Person )的构造函数中传递参数

深拷贝与浅拷贝

数据类型

在开始了解 浅拷贝 与 深拷贝 之前,让我们先来回顾一下 JavaScript 的数据类型(可以参考这里 JavaScript中的数据类型)

在 JavaScript 中,我们将数据分为 基本数据类型(原始值) 与 引用类型

  • 基本数据类型的值是按值访问的,基本类型的值是不可变的
  • 引用类型的值是按引用访问的,引用类型的值是动态可变的

由于数据类型的访问方式不同,它们的比较方式也是不一样的

var a = 100;
var b = 100;

a === b // true

var c = {a: 1, b: 2};
var d = {a: 1, b: 2};

c == d // false 两个不同的对象
  • 基本数据类型的比较是值得比较
  • 引用类型的比较是引用地址的比较

鉴于以上数据类型的特点,我们可以初步想到:所谓 浅拷贝 与 深拷贝 可能就是对于值的拷贝和引用的拷贝(简单数据类型都是对值的拷贝,不进行区分)

一般来说,我们所涉及的拷贝对象,也都是针对引用类型的。由于引用类型属性层级可能也会有多层,这样也就引出了我们所要去了解的 浅拷贝深拷贝

浅拷贝

顾名思义,所谓浅拷贝就是对对象进行浅层次的复制,只复制一层对象的属性,并不包括对象里面的引用类型数据

想象一下,如果让你自己去实现这个功能,又会有怎么的思路呢

首先,我们需要知道被拷贝对象有哪些属性吧,然后还需要知道这些属性都对应了那些值或者地址的引用吧。那么,答案已经呼之欲出了,是的,循环

var person = {
    name: 'tt',
    age: 18,
    friends: ['oo', 'cc', 'yy']
}

function shallowCopy(source) {
    if (!source || typeof source !== 'object') {
        throw new Error('error');
    }
    var targetObj = source.constructor === Array ? [] : {};
    for (var keys in source) {
        if (source.hasOwnProperty(keys)) {
            targetObj[keys] = source[keys];
        }
    }
    return targetObj;
}

var p1 = shallowCopy(person);

console.log(p1)

在上面的代码中,我们创建了一个 shallowCopy 函数,它接收一个参数也就是被拷贝的对象。

  • 首先创建了一个对象
  • 然后 for...in 循环传进去的对象,为了避免循环到原型上面会被遍历到的属性,使用 hasOwnProperty 限制循环只在对象自身,将被拷贝对象的每一个属性和值添加到创建的对象当中
  • 最后返回这个对象

通过测试,我们拿到了和 person 对象几乎一致的对象 p1。看到这里,你是不是会想那这个结果和 var p1 = person 这样的赋值操作又有什么区别呢?

我们再来测试一波

var p2 = person;

// 这个时候我们修改person对象的数据
person.name = 'tadpole';
person.age = 19; 
person.friends.push('tt')

p2.name // tadpole
p2.age // 19
p2.friends // ["oo", "cc", "yy", "tt"]

p1.name // tt
p1.age // 18
p1.friends // ["oo", "cc", "yy", "tt"]

深拷贝

了解完浅拷贝,相信小伙伴们对于深拷贝也应该了然于胸了

浅拷贝由于只是复制一层对象的属性,当遇到有子对象的情况时,子对象就会互相影响。所以,深拷贝是对对象以及对象的所有子对象进行拷贝

实现方式就是递归调用浅拷贝

function deepCopy(source){
   if(!source || typeof source !== 'object'){
     throw new Error('error');
   }
   var targetObj = source.constructor === Array ? [] : {};
   for(var keys in source){
      if(source.hasOwnProperty(keys)){
         if(source[keys] && typeof source[keys] === 'object'){
           targetObj[keys] = source[keys].constructor === Array ? [] : {};
           targetObj[keys] = deepCopy(source[keys]);
         }else{
           targetObj[keys] = source[keys];
         }
      } 
   }
   return targetObj;
}
var obj1 = {
    arr: [1, 2, 3],
    key: {
        id: 22
    },
    func: function() {
        console.log(123)
    }
}

var obj2 = deepCopy(obj1);

obj1.arr.push(4);

obj1.arr // [1, 2, 3, 4]
obj2.arr // [1, 2, 3]
obj1.key === obj2.key // false
obj1.func === obj2.func // true

对于深拷贝的对象,改变源对象不会对得到的对象有影响。只是在拷贝的过程中源对象的方法丢失了,这是因为在序列化 JavaScript 对象时,所有函数和原型成员会被有意忽略

还有一种实现深拷贝的方式是利用 JSON 对象中的 parse 和 stringify,JOSN 对象中的 stringify 可以把一个 js 对象序列化为一个 JSON 字符串,parse 可以把 JSON 字符串反序列化为一个 js 对象,通过这两个方法,也可以实现对象的深复制

// 利用JSON序列化实现一个深拷贝
function deepCopy(source){
  return JSON.parse(JSON.stringify(source));
}
var o1 = {
  arr: [1, 2, 3],
  obj: {
    key: 'value'
  },
  func: function(){
    return 1;
  }
};
var o2 = deepCopy(o1);
console.log(o2); // => {arr: [1,2,3], obj: {key: 'value'}}

实现拷贝的其他方式

  • 浅拷贝
  • Array.prototype.slice()
  • Array.prototype.concat()
  • Object.assign
  • 拓展操作符...
  • ...

深拷贝 很多框架或者库都提供了深拷贝的方式,比如 jQuery 、 lodash 函数库等等,基本实现方式也就和我们前面介绍的大同小异

常用方法

indexOf()

功能:indexOf() 方法返回调用 String 对象中第一次出现的指定值的索引。

语法:indexOf(searchValue, fromIndex)

searchValue:查找的值 formIndex:开始查找的位置 返回值:如果找到了,则返回第一次出现的索引;如果没找到,则返回 -1。

代码:

[1, 3, 1, 4].indexOf(1, 1); // 2
'怪盗 N'.indexOf('我'); // -1

扩展:如果需要查找到最后一次出现指定值的索引,可以使用 lastIndexOf()。 join() 功能:join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串

语法:arr.join(separator)

separator 是合并的形式。例如 '' 就是不以任何形式拼接成字符串:['hello', 'hi'].join('') -> 'hellohi';例如 '-' 就是以 - 形式拼接成字符串:['hello', 'hi'].join('') -> 'hello-hi' 返回值:一个所有数组元素连接的字符串。

代码:

var a = ['Wind', 'Rain', 'Fire'];
var myVar1 = a.join();      // myVar1 的值变为 "Wind,Rain,Fire"
var myVar2 = a.join(', ');  // myVar2的值变为"Wind, Rain, Fire"
var myVar3 = a.join(' + '); // myVar3的值变为"Wind + Rain + Fire"
var myVar4 = a.join('');    // myVar4的值变为"WindRainFire"

map()

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

语法:map((item, index) => {})

item:遍历的项 index:该次遍历项的索引 返回值:一个新数组,每个元素都是回调函数的结果。

代码:

[1, 2, 3, 4].map(item => item * 2) // [2, 4, 6, 8]

[{
  name: 'js',
  age: 24,
}, {
  name: 'N',
  age: 124
}].map((item, index) => {
  return `${index} - ${item.name}`;
}) // ['0 - js', '1 - N']

方法 - pop()

功能:pop() 方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。

语法:

arr.pop():返回从数组中删除的元素 返回值:一个新数组,每个元素都是回调函数的结果。

代码:

let arr = [1, 2, 3, 4];
for(let i = 0, time = 1; i < arr.length; time++) {
  console.log(`------\n第 ${time} 次遍历:`);
  console.log(arr.pop());
  console.log(arr);
}

/* Console:
第 1 次遍历:
4
[ 1, 2, 3 ]
第 2 次遍历:
3
[ 1, 2 ]
第 3 次遍历:
2
[ 1 ]
第 4 次遍历:
1
[]
*/

push()

功能:push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

语法:arr.push(element)

element:需要传入到数组的元素 返回值:当调用该方法时,新的 length 属性值将被返回。

代码:

let arr = [];
arr.push(1);
arr.push('2');
arr.push([3, 4, 5]);
arr.push([...6, 7, 8]);
console.log(arr);

/*
[1, "2", Array(3), 6, 7, 8]
0: 1
1: "2"
2: (3) [3, 4, 5]
3: 6
4: 7
5: 8
length: 6
*/

复制代码 方法 - reduce() 功能:reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

语法:arr.reduce((prev, next) => { return prev + next }

prev:数组前一项的值 next:数组后一项的值 return:return 出来的值,会被当成下一次的 prev 返回值:函数累计处理的结果

代码:

[1, 2, 3, 4].reduce((prev, next) => {
  return prev + next;
}); // 10

reverse()

功能:reverse() 方法将数组中元素的位置颠倒,并返回该数组。该方法会改变原数组。

语法:arr.reverse()

代码:

let arr = [1, 2, 3];
arr.reverse();
console.log(arr); // [3, 2, 1]

slice()

功能:slice() 方法提取一个字符串的一部分,并返回一新的字符串。

语法:str.slice(beginSlice, endSlice)

  • beginSlice:从该索引(以 0 为基数)处开始提取原字符串中的字符。
  • endSlice:结束位置(以 0 为基数)。

返回值:返回一个从原字符串中提取出来的新字符串。

代码:

const str = 'jsliang';
str.slice(0, 2); // 'js'
str.slice(2); // 'liang'

sort()

功能:sort() 对数组的元素进行排序,并返回数组。

语法:sort(function)

function:按某种顺序进行排列的函数。 返回值:排序后的数组。

代码:

[4, 2, 5, 1, 3].sort(), // [1, 2, 3, 4, 5]
[4, 2, 5, 1, 3].sort((a, b) => a < b), // [5, 4, 3, 2, 1]
['a', 'd', 'c', 'b'].sort(), // ['a', 'b', 'c', 'd']
['eat', 'apple'].sort(), // ['apple', 'eat']

splice()

功能:splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

语法:array.splice(start, deleteCount, item1, item2, ...)

start:指定修改的开始位置(从0计数)。 deleteCount:整数,表示要移除的数组元素的个数。 item1, item2, ...:要添加进数组的元素,从start 位置开始。如果不指定,则 splice() 将只删除数组元素。 返回值:由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。

代码:

var months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb');

console.log(months);
// ['Jan', 'Feb', 'March', 'April', 'June']

months.splice(4, 1, 'May');

console.log(months);
// ['Jan', 'Feb', 'March', 'April', 'May']

split()

功能:split() 方法使用指定的分隔符字符串将一个 String 对象分割成字符串数组,以将字符串分隔为子字符串,以确定每个拆分的位置。

语法:str.split('.')

'.':即分割的形式,这里通过 . 来划分,如果是 '' 空,那么就单个字符拆分,如果是 '|',那么通过 | 来划分。

返回值:一个新数组。返回源字符串以分隔符出现位置分隔而成的一个数组。

代码:

String(12321).split(''); // ['1', '2', '3', '2', '1']
String(123.21).split('.'); // ['123', '21']
substring()

substring()

功能:substring() 方法返回一个字符串在开始索引到结束索引之间的一个子集, 或从开始索引直到字符串的末尾的一个子集。

语法:str.substring(start, end)

  • start:需要截取的第一个字符的索引,该字符作为返回的字符串的首字母。
  • end:可选。0 - n,即从 start 开始,截取 end 位长度的字符串。

返回值:包含给定字符串的指定部分的新字符串。

代码:

var str = 'jsliang';
str.substring(0, 2); // js
str.substring(2); // liang

unshift()

功能:unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度。

语法:arr.unshift(element1, ..., elementN)

  • element1:要插入的第一个元素
  • elementN:要插入的第 N 个元素

返回值:当一个对象调用该方法时,返回其 length 属性值。( unshift 会改变原本数组)

代码:

let arrA = ['1'];
arrA.unshift('0');
console.log(arrA); // ['0', '1']

let arrB = [4, 5, 6];
arrB.unshift(1, 2, 3);
console.log(arrB); // [1, 2, 3, 4, 5, 6]