前端面试复习之JS复习

3,480 阅读19分钟

前排提示:感谢@程序员小鹿 写的系列文章 这是我做的他文章的摘抄而已 用于学习

JS基础

原始类型和引用类型(对象类型)

  • 原始类型(6个)
    • String
    • Number
    • Null
    • Undefined
    • Symbol
    • Boolean
  • 引用类型(对象类型)(5个)
    • Object
    • Array
    • Function
    • Date
    • RegExp

数据类型的存储形式

  1. 栈内存(stack)和堆内存(heap)
  2. Stack自动分配内存,Heap动态分配内存
  3. 一般在项目中将对象类型置为null,减少内存消耗
    • 图示:
  4. 原始类型按值存在stack中,可按值直接访问
  5. 对象类型在stack中存放引用地址,在heap中存放具体对象

Null

  1. typeof null === Object
    • javaScript中数据二进制前三位都是0的话,系统就会判定为Object类型。
前三位 类型
000 对象
1 整型
010 双精度
100 字符串
110 布尔

数据类型的判断typeof/instanceof

  • typeof是一元运算符,返回String类型。
  • typeof除了null类型和对象类型不能准确判断,其他都能返回正确类型。
    • 例外:function可以
  • instanceof判断某个对象是不是另一个对象的实例,返回boolean
  • instanceof用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。

类型转换

  • javaScript是弱类型语言,所以特定情况我们需要类型转换
  • 分为显式类型转换和隐式类型转换

显式类型转换

即强制类型转换

String类型

对于原始类型,转String会默认调用toString()方法

String(123)              //"123"
String(true)             //"true"
String(null)             //"null"
String(undefined)        //"undefined"
String([1,2,3])          //"1,2,3"
String(function(){111})  //"function(){111}"
String({})               //[Object Object]

转Boolean类型

除了几个falsy值,''undefinednullNAN0false,其他都转为true

转Number类型

其他类型 数字类型
字符串 1. 数字转化为数字
2.其他转化为NaN
布尔类型 true=>1,false=>0
null 0
undefined NaN
数组 1.数组为空转化为0
2.数组只有一个元素转为该元素
3.其他转化为NaN
空字符串 0
Number(10);        // 10 
Number('10');      // 10 
Number(null);      // 0  
Number('');        // 0  
Number(true);      // 1  
Number(false);     // 0  
Number([]);        // 0 
Number([1,2]);     // NaN
Number('10a');     // NaN
Number(undefined); // NaN

对象类型转原始类型

  • 对象类型转原始类型会调用内置的valueOf()toString()方法
  • 转化原始类型又分为:转化为字符串类型和其他原始类型
    • 如果已经是原始类型,就无须再转化
    • 如果转String,就调用toString()
    • 如果是其他原始类型,就调用valueOf()
    • 如果返回的不是原始类型,继续调用toString()
    • 如果还没有返回原始类型,就报错

隐式类型转换

加法运算

加法运算符是在运行时决定相加还是连接,这被称为"重载"。

如果双方都不是String

  • Boolean+Boolean会转化为数字相加
  • Boolean+Number,布尔转化为数字再加
  • Object+Number,对象类型调用valueOf,如果不是StringBoolean或者Number类型,则继续调用toString()转化为字符串
true + true  // 2
1 + true     // 2
[1] + 3      // '13' 

数组的valueOf()返回的还是数组本身,所以会继续调用toString()

字符串和字符串以及字符串与非字符串相加都会变连接

1 + 'b'     // '1b'
false + 'b' // 'falseb'
'1' + 1     //'11'

其他运算

其他算术运算符,减法除法乘法一律全部转为数字,再运算

1 * '2'  // 2
1 * []   // 0

逻辑运算符

条件判断

  • &&:所有条件为真,才为真
  • ||:只要一个条件真,就真

赋值操作

  • A&&B

如果A真就过,赋值为B;如果A假就不过,直接就是A

  • A||B

如果A为真就直接是A,如果A为假就为B

比较运算符 =====

===是严格意义上的相等,必须类型和值都相等。

==先判断两边类型是否相等,如果不等,先转换类型,再判断值是否相等。

this

this就是一个对象,不同情况this指向不同。

  • 对象调用,this指向该对象
var obj = {
    name:'小鹿',
    age: '21',
    print: function(){
        console.log(this)
        console.log(this.name + ':' + this.age)
    }
}

// 通过对象的方式调用函数
obj.print();        // this 指向 obj
  • 直接调用的函数,this指向window对象
function print(){
	console.log(this);
}
// 全局调用函数
print();   // this 指向 window
  • 通过new 的方式,this永远指向新创建的对象
function Person(name,age){
  this.name = name
  this.age = age
  console.log(this)
}

var xiaohu = new Person('小胡',24)// this = > xiaohu
  • 箭头函数中的this

箭头函数没有单独的this值,其this与声明所在的上下文相同。也就是说调用箭头函数的时候,不会隐式调用this参数,而是从定义时的函数继承上下文

const obj = {
    a:()=>{
        console.log(this);
    }
}
// 对象调用箭头函数
obj.a(); // window

改变this的指向

可以通过调用函数的callapplybind来改变this指向

var obj = {
    name:'小鹿',
    age:'22',
    adress:'小鹿动画学编程'
}

function print(){
    console.log(this);       // 打印 this 的指向
    console.log(arguments);  // 打印传递的参数
}

// 通过 call 改变 this 指向
print.call(obj,1,2,3);   

// 通过 apply 改变 this 指向
print.apply(obj,[1,2,3]);

// 通过 bind 改变 this 的指向
let fn = print.bind(obj,1,2,3);
fn();

共同点:

  • 三者都能改变this指向,且第一个参数都是this指向的对象
  • 三者都采用后续传参的形式

不同点:

  • call的传参是单个传递,apply后续参数是数组形式,bind都可以
  • callapply是直接执行,bind会返回一个函数,在调用时才会执行

箭头函数不能用callapply改变this指向,因为他没有自己的this指向,如果用callapply只会传递后续参数。

new关键字

new的过程包括四个阶段:

  • 创建一个新对象。
  • 新对象的__proto__属性指向原函数的prototype属性(继承原函数的原型)
  • 将新对象绑定到此函数的this上。
  • 返回新对象
// new 生成对象的过程
// 1、生成新对象
// 2、链接到原型
// 3、绑定 this
// 4、返回新对象
// 参数:
// 1、Con: 接收一个构造函数
// 2、args:传入构造函数的参数
function create(Con, ...args){
    // 创建空对象
    let obj = {};
    // 设置空对象的原型(链接对象的原型)
    obj._proto_ = Con.prototype;
    // 绑定 this 并执行构造函数(为对象设置属性)
    let result = Con.apply(obj,args)
    // 如果 result 没有其他选择的对象,就返回 obj 对象
    return result instanceof Object ?  result : obj;
}
// 构造函数
function Test(name, age) {
    this.name = name
    this.age = age
}
Test.prototype.sayName = function () {
    console.log(this.name)
}

// 实现一个 new 操作符
const a = create(Test,'小鹿','23')
console.log(a.age)

创建对象的方式

常用创建对象的方式:

  • new
  • 字面量

其他创建对象的方式:

  • Object.create()

字面量创建对象的优势:

  • 代码量少,易读
  • 运行速度更快,可以在解析时被优化,不像new一样,解析器需要顺着作用域链从当前作用域开始查找,如果在当前作用域找到了名为Object()的函数就执行,如果没找到就顺着继续向上找,直到找到全局Object()构造函数为止。
  • Object()构造函数可以接受参数,通过这个参数可以吧对象实例的创建过程委托给另一个内置构造函数,并返回另外一个对象实例,而这往往不是想要的。对于Object.create()方式:Object.create(proto,[propertiesObject])
    • proto:新创建对象的原型对象
    • propertiesObject:可选,可为创建的新对象设置属性和值 一般用于继承:
var People = function (name){
  this.name = name;
};

People.prototype.sayName = function (){
  console.log(this.name);
}

function Person(name, age){
  this.age = age;
  People.call(this, name);  // 使用call,实现了People属性的继承
};

// 使用Object.create()方法,实现People原型方法的继承,并且修改了constructor指向
Person.prototype = Object.create(People.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: Person,
    writable: true
  }
});

Person.prototype.sayAge = function (){
  console.log(this.age);
}

var p1 = new Person('person1', 25);
 
p1.sayName();  //'person1'
p1.sayAge();   //25

new/字面量与Object.create(null)创建对象

  • new和字面量创建的对象的原型指向Object.prototype,会继承Object的属性和方法
  • Object.create(null)创建的对象,其原型指向nullnull作为原型链的顶端,没有也不会继承任何属性和方法

闭包

作用域

规定变量和函数的可使用范围叫做作用域。

每个函数都会有一个作用域,查找变量或函数时,由局部作用域到全局作用域依次查找,这些作用域的集合就叫做作用域链。

闭包

函数执行,形成一个私有的作用域,保护里面的私有变量不受外界干扰,除了保护私有变量外,还可以保存一些内容,这种模式叫做闭包。

内存回收机制

不再用到的内存,系统就回收。

内部函数引用着外部的函数的变量,外部函数尽管执行完毕,作用域也不会销毁。从而形成了一种不销毁的私有作用域。

作用域继承

内部函数可以访问外部函数作用域,外部函数不能获取内部函数的作用于变量。

通俗的闭包

一个函数里边再定义一个函数,内部函数一直保持有对外部函数作用域的访问权限。

两个作用:保护和保存。

保护的应用
  • 团队开发,把自己的代码放在私有作用域中,防止变量命名冲突;把需要提供给别人的方法,通过return或者window.xxx暴露在全局。
  • 封装私有变量
  • jQuery就使用了保护机制
保存的应用
  • 选项卡闭包

循环绑定事件

// 事件绑定引发的索引问题
var btnBox = document.getElementById('btnBox'),
    inputs = btnBox.getElementsByTagName('input')
var len = inputs.length;
for(var i = 0; i < len; i++){
    inputs[i].onclick = function () {
        alert(i)
    }
}

运行程序得出的结果都是len的数值。

因为所有事件绑定都是异步的,当触发点击事件,执行方法的时候,循环早就结束了。

同步:JS中当前这个任务没完成,下面的任务都不会执行,只有等当前彻底完成,才会执行下面的任务。 异步:JS当前任务没有完成,需要等一会再完成,但此时我们可以继续执行下面的任务。

解决方案:

当点击事件执行的时候,就会在私有作用域查找i的值,此时私有作用域没有i,就会去全局作用域查找,此时全局作用于的i已经被改变了,所以要创建一个私有作用域的i。

for(var i = 0;i<length;i++){
    ~function(i){
        inputs[i].onclick = function(){
            alert(this.myIndex)
        }
    }(i)
}

闭包既有优点也有缺点。

优点是通过闭包解决循环几次就创建几个私有作用域,然后每个私有作用域都有一个私有变量i。

缺点就是生成多个不销毁的私有作用域,对性能有影响。

原型和原型链

原型:每个JS对象都有__proto__属性,这个属性指向了原型。

原型链:多个对象通过__proto__的方式连接起来。

instanceof的原理

通过判断该对象的原型链中是否可以找到该构造类型的prototype类型

继承

经典继承(构造函数)

//1、当用调用 call 方法时,this 带边 son 。
//2、此时 Father 构造函数中的 this 指向 son。
//3、也就是说 son 有了 colors 的属性。
//4、每 new 一个 son ,都会产生不同的对象,每个对象的属性都是相互独立的。
function Father(){
	this.colors = ["red","blue","green"];
}

function Son(){
    // this 是通过 new 操作内部的新对象 {} ,
    // 此时 Father 中的 this 就是为 Son 中的新对象{}
    // 新对象就有了新的属性,并返回得到 new 的新对象实例
    // 继承了Father,且向父类型传递参数
	Father.call(this);
}

let s = new Son();
console.log(s.color)

基本思想:在子类的构造函数的内部调用父类的构造函数。 优点:

  • 保证了原型链中引用类型的独立,不被所有实例共享。
  • 子类创建的时候可以向父类传参。

缺点:

  • 继承的方法都在构造函数中定义,构造函数不能复用。
  • 父类中的方法对子类而言是不可见的,子类所有属性都定义在父类的构造函数中。

组合继承

function Father(name){
	this.name = name;
	this.colors = ["red","blue","green"];
}

// 方法定义在原型对象上(共享)
Father.prototype.sayName = function(){
	alert(this.name);
};

function Son(name,age){
    // 子类继承父类的属性  
	Father.call(this,name);     //继承实例属性,第一次调用 Father()
    // 每个实例都有自己的属性
	this.age = age;
}

// 子类和父类共享的方法(实现了父类属性和方法的复用)                              
Son.prototype = new Father();   //继承父类方法,第二次调用 Father()

// 子类实例对象共享的方法
Son.prototype.sayAge = function(){
	alert(this.age);
}

var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5

var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10

基本思想:

  • 使用原型链实现对原型对象属性和方法的继承
  • 借用构造函数来实现对实例属性的继承

优点:

  • 在原型兑现上定义的方法实现了函数的复用。
  • 每个实例都有属于自己的属性。

缺点:组合继承调用了两次父类的构造函数,造成了不必要的消耗。

原型继承

function object(o){
	function F(){}
	F.prototype = o;
    // 每次返回的 new 是不同的
	return new F();
}

var person = {
	friends : ["Van","Louis","Nick"]
};

// 实例 1
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");

// 实例 2
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");

// 都添加至原型对象的属性(所共享)
alert(person.friends); // "Van,Louis,Nick,Rob,Style"

基本思想:创建临时的构造函数,将传入的对象作为该构造函数的原型对象,然后返回新构造函数的实例。

浅拷贝:object产生的对象是不相同的,但是原型对象都是person对象,所改变存在原型对象的属性被所有生成的实例所共享,不仅Person拥有,而且子类生成的实例也共享。

Object.create():在ECMAScript5中新增了此方法。

  • 参数一:新对象的原型对象
  • 参数二:新对象定义的额外的属性。(可选)

寄生式继承

function createAnother(original){
	var clone = object(original); // 通过调用object函数创建一个新对象
	clone.sayHi = function(){ // 以某种方式来增强这个对象
		alert("hi");
	};
	return clone; //返回这个对象
}

基本思想:不必为了指定子类的原型而调用父类的构造函数。

优点:解决组合继承中两次调用构造函数的开销。

垃圾回收机制

内存泄漏

不再用到的内存,没有及时释放,就是内存泄漏。

之所以会有垃圾回收机制,是因为js中字符串、对象、数组等只有确定固定大小时,才会动态分配内存,而使用完必须释放,否则会消耗完所有内存。

js与其他语言不同,具有自动垃圾收集机制。

两种垃圾回收策略

找出那些不再使用的变量,然后释放内存,垃圾回收器会按照固定的时间间隔,周期性的执行垃圾回收。

标记清除法

垃圾回收器在运行的时候,会给存储在内存中的所有变量都加上标记,然后会去掉环境中变量以及被环境中的变量引用的变量的标记。剩下的就被视为要删除的变量。

其实现原理就是通过判断一个变量是否在执行环境中被引用,来进行标记删除。

引用计数法

跟踪记录每个值被引用的次数。

当声明变量并将一个引用类型的值赋值给该变量时,则这个值引用次数加1,同一值被赋予另一个变量,该值引用次数加1。当引用该值的变量被另一个值取代,则引用计数减1,当计数为0时,就无法访问了,就会收回。

缺陷:两个对象的相互循环引用时,在函数执行完成后,两个对象相互引用计数并未归零,所有无法回收。

常见的就是在IE BOM和DOM中,使用的对象并不是js对象,所以垃圾回收是基于计数策略。

如何管理内存

虽然js内存都是自动管理,但还是有问题,比如分配给web浏览器的可用内存数量通常比分配给桌面应用的少。

为了能让页面获得最好的性能,必须确保js变量占用最少的内存,所以最好将不用的变量引用释放,也叫解除引用。

  • 对于局部变量,函数执行完成离开环境变量,变量将自动解除。
  • 对于全局变量,需要手动解除。此时只是解除引用,下一次垃圾回收将其回收。
var a = 20;
alert(a + 100);
var a = null;

只有与环境变量失去引用的变量才会被标记回收,将对象引用设置为null,就失去引用,等待被回收。

深拷贝和浅拷贝

对基本类型的拷贝就是对值进行拷贝,而对于引用类型来说,拷贝的不是值,而是值的地址,最后两个变量的地址指向的是同一个值。

var a = 10;
var b = a;
b = 30;
console.log(a); // 10值
console.log(b); // 30值

var obj1 = new Object();
var obj2 = obj1;
obj2.name = "小鹿";
console.log(obj1.name); // 小鹿

要想将obj1,obj2的关系断开,不让其指向同一个地址,分为浅拷贝和深拷贝。

  • 浅拷贝:只进行一层关系的拷贝。
  • 深拷贝:进行无限层的拷贝。

自己实现一个浅拷贝:

function shallowClone(o){
    const obj = {};
    for(let i in o){
        obj[i] = o[i]
    }
    return obj;
}
  • 扩展运算符实现:
let a = {c: 1}
let b = {...a}
a.c = 2
console.log(b.c) // 1
  • Object.assign()实现
let a = {c: 1}
let b = Object.assign({}, a)
a.c = 2
console.log(b.c) // 1

深拷贝需要在浅拷贝的基础上加上递归

比较简单的实现方法:利用JSON.parse(JSON.stringify(obj))

function clonebyJSON(source){
    return JSON.parse(JSON.stringify(source))
}

但他内部也是使用的递归,递归到一定深度会爆栈,但不会出现循环引用问题。

异步编程

由于JavaScript是单线程,所以会有阻塞问题,当一个任务执行完成后才能执行下一个任务,这样就会出现页面卡死。

单线程是由一些与用户的互动以及操作DOM相关的操作决定了JS要使用单线程,否则会带来复杂的同步问题。需要加锁。

H5标准规定允许js创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。

实现异步编程

最早是使用回调函数,回调函数不是直接调用,而是在特定的事件或条件发生时另一方调用,用于对该事件或条件进行响应。

比如Ajax回调:

$.ajax({
    type:'post',
    url:'test.json',
    dataType:'json',
    success:function(res){
        //成功回调
    },
    fail:function(err){
        //响应失败回调
    }
})

但是如果某个请求存在依赖性:

$.ajax({
    type:'post',
    url:'test.json',
    dataType:'json',
    success:function(res){
        $.ajax({
            type:'post',
            url:'xxx?id='+res.id,
            success:function(res){
                $.ajax({
                    ...//循环
                })
            }
        })
    },
    fail:function(err){
        //响应失败回调
    }
})

这样就会不断循环嵌套,称为回调地狱。

缺点:

  • 嵌套函数存在耦合性,一旦有改动就都得改。
  • 嵌套函数多,很难处理错误。
  • 回调函数不能使用try catch捕获异常(异常捕获只能在函数执行的时候才能捕获到)
  • 回调函数不能直接return

为什么不能捕获异常?

这和js运行机制相关,异步任务执行完成会加入任务队列,当执行栈中没有可执行任务了,主线程去除任务队列中的异步任务并入栈执行,当异步任务执行时,捕获异常的函数已经在执行栈内退出了,所以异常无法捕获。

为什么不能return

return只能终止回调函数的执行,而不能终止外部代码的执行。

解决回调地狱

ES6给了三种解决方案:Generator、Promise、async/await

EventLoop事件循环机制

执行上下文

可以理解为代码执行的环境。

JS执行上下文分为三种:

  • 全局执行上下文:全局this指向的window,可以是外部加载的js文件或者本地script标签中的代码
  • 函数执行上下文:局部上下文,每个函数被调用的时候,都会创建一个局部上下文
  • Eval执行上下文
执行栈

代码执行的时候,遇到一个执行上下文就将其依次压入执行栈。

当代码执行时,先执行位于栈顶的执行上下文中的代码,当栈顶的执行上下文代码执行完毕后就出栈,然后执行下一个位于栈顶的执行上下文。

宏任务

一般包括:

  • 整体script标签内的代码
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
微任务

一般包括:

  • Promise
  • process.nextTick(Node)
  • MutationObserver

nextTick队列会比Promise先执行

事件循环运行机制
  1. 事件循环机制是从script标签内的代码开始,作为一个宏任务处理。
  2. 在代码执行过程中,如果遇到宏任务,比如setTimeout,就将当前任务分发到对应的执行队列中去。
  3. 遇到微任务,如Promise,在创建Promise实例对象时,代码顺序执行,如果到了执行then操作,该任务就被分发到微任务队列中去。
  4. script标签内的代码执行完毕,执行过程中所涉及的宏任务和微任务也都被分配到相应的队列中去。
  5. 宏任务执行完毕,去微任务队列执行所有存在的微任务。
  6. 微任务执行完毕,第一轮消息循环执行完毕,页面渲染一次。
  7. 开始第二轮消息循环,从宏任务队列中取出任务执行。
  8. 如果两个任务队列没有任务执行了,所有任务执行完毕。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>消息运行机制</title>
</head>
<body>

</body>
    <script>
        console.log('1');
        setTimeout(() => {
            console.log('2')
        }, 1000);
        new Promise((resolve, reject) => {
            console.log('3');
            resolve();
            console.log('4');
        }).then(() => {
            console.log('5');
        });
        console.log('6');// 1,3,4,6,5,2
    </script>
</html>
  • 首先script代码宏任务
  • 输出1
  • 遇到setTimeout 加入宏任务队伍
  • 遇到Promise,构造时是同步的,输出3,4,到then加入微任务
  • 继续 输出6
  • script执行完,开始执行微任务队列
  • 只有一个 输出5
  • 微任务执行完页面渲染依次
  • 下一次循环开始,从宏任务开始,setTimeout 输出2
  • 全为空