自备面试-JS基础

108 阅读10分钟

css相关 flex 300ms延迟

原始类型和对象类型:

在JavaScript中,每一个变量在内存中都需要一个空间来存储。内存空间又被分为两种,栈内存与堆内存。
基本类型数据保存在栈内存中:存储的值大小固定(不可变,改变实际上是新分配了一块内存)
引用类型保存在堆内存中:

  • 存储的值大小不定,可动态调整
  • 空间较大,运行效率低
  • 无法直接操作其内部存储,使用引用地址读取 引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值。

类型判断

  • typeOf可用来判断七大基本类型(number,string,boolen,object,function,null,symbol)
  • instanceof判断一个实例是否属于某种类型(right的prototype是否在left的原型链上)
  • 想要准确判断类型 可以用Object.prototype.toString.call();

深浅拷贝

浅拷贝: 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值,
如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。(注意是属性,不是对象本身)
方法:Object.assign,扩展运算符,array.slice,array.concat等

/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}

深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象 JSON.parse(JSON.stringify());
不能解决拷贝其他引用类型、拷贝函数、循环引用等情况
手写:

const clone = (target) => {
    if (typeof target === 'object') {
        const cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    }
    else {
        return target;
    }
}
let test = {
    field1: 1,
    field2: { a: 1 }
};
test.test = test;
const res = clone(test); //此处会报错 栈溢出

此为最初版本 但如果传入的对象循环自引用 就会导致函数无限被调用
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

const clone = (target, map = new WeakMap()) => {
    if (typeof target === 'object') {
        const cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    }
    else {
        return target;
    }
}


接下来,我们可以使用,WeakMap替代Map来使代码达到画龙点睛的作用。
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
什么是弱引用呢?
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

如果我们使用Map的话,那么对象间是存在强引用关系的:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;

虽然我们手动将obj,进行释放,然而target依然对obj存在强引用关系,所以这部分内存依然无法被释放。

WeakMap:存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。

原型和原型链

prototype

每个函数都有prototype属性 调用构造函数创建对象时,创建的实例的原型
每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性

proto

每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型

constructor

每个原型都有一个 constructor 属性指向关联的构造函数


相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线

继承

构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。 !

原型链继承

如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函 数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

function SuperType() { 
 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
} 
// 继承 SuperType 
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function () {
	return this.subproperty; 
}; 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // true

执行上下文

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

执行上下文栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文
初始化时压入全局上下文,当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出

变量对象

  • 变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
  • 全局上下文中的变量对象就是全局对象,函数上下文中,用活动对象(activation object, AO)来表示变量对象。
  • 活动对象和变量对象其实是一个东西,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,各种属性才能被访问。
  • 执行上下文的代码会分成两个阶段进行处理:分析和执行
  • 分析:给变量对象添加形参(key和value)、函数声明、变量声明等初始的属性值,声明提升就是这么来的
  • 执行:修改变量对象的属性值

作用域链

  • 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
  • 函数的作用域在函数定义的时候就决定了。这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,但是注意:[[scope]] 并不代表完整的作用域链!
  • 当函数激活时,就会将活动对象添加到作用链的前端,Scope = [AO].concat([[Scope]]);至此,作用域链创建完毕。

this

原理,为什么会有this:

var obj = { foo: 5 };

JavaScript 引擎会先在内存里面,生成一个对象 { foo: 5 },然后把这个对象的内存地址赋值给变量 obj。

var obj = { foo: function () {} };

JavaScript 引擎会将函数单独保存在内存中,然后再将函数的地址赋值给 foo 属性的 value 属性。
函数是一个单独的值(以地址形式赋值),所以才可以在不同的环境中执行
又因为JavaScript 允许在函数体内部,引用当前环境的其他变量。所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

具体场景

优先级从低到高分别如下:

  1. 默认的 this 绑定, 在一个函数中使用了 this, 但是没有为 this 绑定对象. 这种情况下, 非严格默认, this 就是全局变量 Node 环境中的 global, 浏览器环境中的 window.
  2. 隐式绑定: 使用 obj.foo() 这样的语法来调用函数的时候, 函数 foo 中的 this 绑定到 obj 对象.
  3. 显示绑定: foo.call(obj, ...), foo.apply(obj,[...]), foo.bind(obj,...)
  4. 构造绑定: new foo() , 这种情况, 无论 foo 是否做了绑定, 都要创建一个新的对象, 然后 foo 中的 this 引用这个对象. PS:箭头函数:根据外层(函数或者全局)作用域(词法作用域)来决定this。

闭包

  • 定义:能访问自由变量(既不是函数参数也不是局部变量)的函数
  • 实践角度:创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 原理: 子函数创建时,作用域链上保存了父函数的活动对象AO,即使父函数的上下文被销毁了,JS依然会让父函数的AO活在内存中,所以子函数依然可以通过作用域链找到它,从而实现闭包

EventLoop

javascript是一门单线程的非阻塞的脚本语言。

  • 单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。
  • 非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。 当调用一个方法的时候,js会生成执行上下文(包括this,变量对象,作用域链),这些上下文会被依次放入执行栈(stack)中。
    当遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列(queue)。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕,去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
  • 宏任务: setInterval()
    setTimeout()
  • 微任务 new Promise()
    new MutaionObserver()

当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。