Skip to content

JavaScript 高级

能学完这里,才是JS高手

作用域及作用域链

作用域就是一个独立的地盘,能够访问和修改里面的值,并且变量不会外泄,不同作用域中同名变量也不会冲突 Es6之前只有全局作用域和函数作用域,Es6新增了块级作用域(let 和 const)

如图,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的全局作用域为止,如果还是没有找到就报错 而这一层一层嵌套起来的作用域,就形成了作用域链 做道题,理解作用域

jsx
function foo(){
    console.log(a);
}
function bar(){
    var a = 3; foo();
}
var a = 2;
bar()

垃圾回收机制

垃圾产生&为何回收

我们知道写代码时创建一个基本类型、对象、函数……都是需要占用内存的,但是我们并不关注这些,因为这是引擎为我们分配的,我们不需要显式手动的去分配内存 但是,你有没有想过,当我们不再需要某个东西时会发生什么?JavaScript 引擎又是如何发现并清理它的呢? 我们举个简单的例子

jsx
 let test = { name: "isboyjc" };
 test = [1,2,3,4,5]

如上所示,我们假设它是一个完整的程序代码 我们知JavaScript 的引用数据类型是保存在堆内存中的,然后在栈内存中保存一个对堆内存中实际对象的引用,所以,JavaScript 中对引用数据类型的操作都是操作对象的引用而不是实际的对象。 可以简单理解为,栈内存中保存了一个地址,这个地址和堆内存中的实际值是相关的 那上面代码首先我们声明了一个变量 test,它引用了对象 {name: ‘isboyjc’},接着我们把这个变量重新赋值了一个数组对象,也就变成了该变量引用了一个数组,那么之前的对象引用关系就没有了,如下图

没有了引用关系,也就是无用的对象,这个时候假如任由它搁置,一个两个还好,多了的话内存也会受不了,所以就需要被清理(回收) 用官方一点的话说,程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃

垃圾回收策略

在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收 至于如何回收,其实就是怎样发现这些不可达的对象(垃圾)它并给予清理的问题, JavaScript 垃圾回收机制的原理说白了也就是定期找出那些不再用到的内存(变量),然后释放其内存 你可能还会好奇为什么不是实时的找出无用内存并释放呢?其实很简单,实时开销太大了 我们都可以 Get 到这之中的重点,那就是怎样找出所谓的垃圾? 这个流程就涉及到了一些算法策略,有很多种方式,我们简单介绍两个最常见的:标记清除算法 引用计数算法

闭包

定义:闭包是指一个函数有权访问外部作用域中的变量,这个函数就是闭包,所以 所有的 JS 函数都是闭包,因为他们都是对象,都关联到了作用域链 优点:内部函数有权访问外部函数的局部变量 缺点:内部函数引用的变量会在内存中,不会立刻销毁; 因为内部函数有权访问外部函数,所以外部函数执行完了也不会被垃圾回收,而占用内存; 如果闭包用得太多会导致性能降低

对闭包的理解:方法里面返回一个方法,应用场景在延长变量作用域、创建私有环境

变量和函数提升

函数剩余参数和展开运算符

ES6的箭头函数

数组解构

对象解构

forEach

构造函数

new的实例化执行过程

实例成员和静态成员

基本包装类型

Object静态方法

数组reduce累计方法

数组find、every

字符串常见方法

原型/原型链

原型

我们都知道new了一个新的实例之后,我们什么都没做就可以直接访问toString(),valueOf()等一些方法,那这些方法是从哪来的呢? 答案就是原型,来我们先看一张图 对照图片,我们看几行代码

js
function Parent(){} 
// 这就是构造函数
let child = new Parent() 
// child就是实例
Parent.prototype.getName = function(){
    console.log('沐华')
}
// getName是构造函数的原型对象上的方法 
child.getName()// '沐华' 这是继承来的方法

prototype :它是构造函数的原型对象。每个函数都会有这个属性,强调一下,是函数,其他对象是没有这个属性的

proto :它指向构造函数的原型对象。每个对象都有这个属性,强调一下,是对象,同样,因为函数也是对象,所以函数也有这个属性。不过访问对象原型(child.proto)的话,建议用Es6的Reflect.getPrototypeOf(child)或者Object.getPrototypeOf(child)方法 constructor :这是原型对象上的指向构造函数的属性,也就是说代码中的 Parent.prototype.constructor === Parent 是为 true 的

对原型的理解

原型对象prototype 函数特有 JS通过原型实现多个对象的继承

原型污染

原型污染是指攻击者通过某种手段修改js的原型

jsx
Object.prototype.toString = function () {
    alert('原生方法被改写,已完成原型污染')
};

constructor属性 对象原型__proto__

原型继承

上面说了对象之间有一个原型对象指针proto关联,形成链式结构,所以一个对象就可以通过这个关联访问另一个对象的属性和函数,这就是继承 ES6 继承

jsx
class Parent(){
    constructor(props){
        this.name = '沐华'
    }
}
// 继承
class Child extends Parent{
    // props是继承过来的属性, myAttr是自己的属性  
    constructor(props, myAttr){
        // 调用父类的构造函数,相当于获得父类的this指向       
        super(props)
    }
}
console.log(new Child().name) // 沐华

虽然现在都用 ES6 的 class,但是 ES5 的继承面试还是会问 ES5 继承 ES5 的继承方式有很多种,什么原型链继承、组合继承、寄生式继承…等等,了解一种面试就够用了

jsx
function Parent(){}
Parent.prototype.getName = function(){
    return '沐华'
}
function Child(){}
// 方式一
Child.prototype = Object.create(Parent.prototype) 
Child.prototype.constructor = Child
// 重新指定 constructor
// 方式二
Child.prototype =Object.create(Parent.prototype,{
    constructor:{
        value: Child,
        writable: true,
        // 属性能不能修改       
        enumerable: true, 
        // 属性能不能枚举(可遍历性),比如在 for in/Object.keys/JS stringify        
        configurable: true, // 属性能不能修改属性描述对象和能否删除    
    }
})
console.log(new Child().getName()) // 沐华

原型链以及instanceOf

原型链

每个对象都有一个_proto_属性指向原型对象,原型对象也是对象,所以也有_proto_指向原型对象的原型对象,一层一层往上,形成起来的链式关系,就是原型链 原型链也决定了js中的继承方式,当我们访问一个属性时:

先访问对象的实例属性,找到就返回,没有就通过proto去原型对象中找 在原型对象上找到,就返回,没有继续通过原型的proto向上层查找 一直到Object.prototype,找到就返回,没有就返回undefined,不找了 原型链的最上层对象就是Object,那Object构造函数的原型是谁? 答案是自身,它的constructor指向Object,而它的_proto_则指向null

instanceof

判断是否出现在该类型原型链中的任何位置,判断引用类型可还行?一般判断一个变量是否属于某个对象的实例

jsx
console.log(null instanceof Object) // false
console.log({} instanceof Object) // true
console.log([] instanceof Object) // true
console.log(function(){} instanceof Object) // true

递归函数

原理就是递归遍历对象/数组,直到里面全是基本类型为止再复制 需要注意的是 属性引用了自身的情况,就会造成循环引用,导致栈溢出 解决循环引用 可以额外开僻一个存储空间,存储当前对象和拷贝对象的关系,当需要拷贝对象时,先去存储空间找,有木有这个拷贝对象,如果有就直接返回,如果没有就就继续拷贝,这就解决了 这个存储空间可以存储成key-value的形式,且key可以是引用类型,选用Map这种数据结构。检查map中有木有克隆过的对象,有就直接直接返回,没有就将当前对象作为key,克隆对象作为value存储,继续克隆

jsx
function clone(target, map = new Map()){
    if (typeof target === 'object') {
         // 引用类型才继续深拷贝        
         let obj = Array.isArray(target) ? [] : {}
        // 考虑数组        // 防止循环引用        
        if (map.get(target)) {
            return map.get(target)
            // 有拷贝记录就直接返回        
        }
        map.set(target,obj)
        // 没有就存储拷贝记录        
        for (let key in target) {
            obj[key] = clone(target[key])
            // 递归        
        }
        return obj
    } else {
        return target
    }
}

深拷贝实现

拷贝栈也拷贝堆,重新开僻一块内存

jsx
JSON.parse(JSON.stringify())
let obj1 = { a:1, b:{ c:3 } }
let obj2 = JSON.parse(JSON.stringify(obj1))
obj1.a = 'a'
obj1.b.c = 'c'
console.log(obj1) // { a:'a', b:{ c:'c' } }
console.log(obj2) // { a:1, b:{ c:3 } }

该方法可以应对大部分应用场景,但是也有很大缺陷,比如拷贝其他引用类型,拷贝函数,循环引用等情况

利用lodash和json实现深拷贝

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。lodash官方文档

异常处理

普通函数和箭头函数的this

call

apply

bind

防抖与节流