JS基础回顾

208 阅读7分钟

数据结构中的栈

栈是一组数据的存放方式;特点是先进后出,后进先出

模拟栈结构特点

class Stack {
  private stackArr: number[] = []
  //添加新元素到栈顶
  push(element: number) {
    this.stackArr.push(element);
  }
  //移动栈顶元素,同时返回移除的元素
  pop(element): number {
    return this.stackArr.pop();
  }
}

let stack = new Stack() 
stack.push(1)
stack.push(2)
console.log(stack) //Stack { items: [ 1, 2 ] }
console.log(stack.pop()); //2

  • 一般来说,系统会划分出两种不同的内存空间,一种叫做stack (栈),另一种叫做heap(堆)
    • stack是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小
    • heap是没有结构的,数据可以任意存放。因此,stack的寻址速度要快于heap
  • 只要是局部的、占用空间确定的数据,一般都存放在stack里面,否则就放在heap里面,所有的对象都存放在heap

队列

  • 队列是一种操作受限制的线性表
  • 只允许在表的前端队头进行删除操作,或者在表的后端队尾进行插入操作
  • 队列的特点是先进先出

模拟队列特点

class Queue {
    private items: number[] = []
    enqueue(element: number) {
        this.items.push(element)
    }
    dequeue() {
        return this.items.shift();
    }
}

数据类型

  • 基本数据类型:Number, String, Boolean, Null, undefined, Symbol
  • 引用类型:Object, [], /^$/, Math, new Date()...

执行上下文

  • 当函数运行时,会创建一个执行环境,这个执行环境就叫执行上下文(Execution Context)
  • 执行上下文中会创建一个对象叫做变量对象(Variable Object),基础数据类型都保存在变量对象中
  • 基本数据类型存储在栈中;引用数据类型存储在堆中,引用数据的地址存储在栈中

如何复制

  • 当我们复制基本数据类型时,复制的是值本身
  • 当我们复制的是引用数据类型时,复制的是当前指向堆中的地址

执行上下文生命周期

一个新的执行上下文的生命周期有两个阶段:

  • 编译阶段:创建变量对象;初始化作用域链;初始化this 指向;初始化arguments...; 寻找里面的var 变量声明、函数声明并赋值,进行变量提升
  • 执行阶段:变量赋值;函数赋值;代码执行
/* 当执行getName的时候,会创建一个执行上下文
**  编译阶段
**  1、处理参数,把参数放入VO	 
**  2、扫描所有代码,找出function 声明,从上往下依次执行,在编译阶段,会处理所有的函数声明,
如果有重复的声明,后面会覆盖前面的声明
**  3、扫描var 关键字, var 只声明不赋值,值是undefined
**  4、在编译阶段不会let变量的,let变量也不会放在VO里
**  编译完成
*/
var name = '小妖'
function getName() {
  let name = '现世'
  console.log(name);
}
getName()

作用域链

作用域链在函数创建的时候就已经确定了,跟在哪执行没有关系

闭包

简单来说是执行和被调用不在同一个上下文。

  • 闭包有两部分组成,一个是当前的执行上下文,一个是在该执行上下文中创建的函数
  • 当函数执行的时候引用了当前执行上下文中的变量就会产出闭包
  • 在现代浏览器中当一个值失去引用的时候就会被标记,被垃圾收集回收机回收并释放空间; 闭包的本质就是在函数外部保持内部变量的引用,阻止垃圾回收

闭包的作用: 保存/保护

  1. 为了防止全局变量的冲突污染,我们建议每个开发者,都把自己的代码放置到一个闭包中(自执行函数执行即可,这样就是私有的上下文)
  2. 如果我们封装一个插件或者类库,为了防止我们定义的变量和方法和用户定义的冲突,是需要我们把所有写的代码放到一个闭包中

var/let/const

  • 创建变量的六种方式中:var/function有变量提升,而let/const/class/import都不存在这个机制
  • var允许重复声明,let不允许;在相同的作用域中(或执行上下文中),如果使用var/function关键词声明变量并且重复声明,是不会有影响的(声明第一次之后,之后再遇到就不在重复声明了);但是使用let/const就不行,浏览器会校验当前作用域中是否已经存在这个变量,如果已经存在了,则再次基于let等重新声明就会报错
  • let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问
  • 在let声明变量前不能使用该变量,这特性叫暂时性死区(temporal dead zone)

this

  • 函数的this是在被调用的时候才能确定的
  • 非严格模式下this就是window,严格模式下this是undefined
  • 如果是为DOM元素绑定事件,那么绑定的方法中的this一般是元素本身
  • 构造函数中this 是当前实例
  • 箭头函数没有自己的this,无法创建箭头函数的实例

call,apply,bind

//call
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
      result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};
//apply
Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};
//bind
Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
      fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

原型链

  • 函数Function自带一个prototype属性,属性值是一个对象,存储实例公用的属性和方法(也叫原型)
  • 每一个原型(prototype)自带constructor属性,这个属性存储的是当前函数本身
function fn() {}

fn.prototype.constructor === fn //true
  • 每一个对象数据类型的值自带一个__proto__属性,属性值指向所属类的原型protorype

继承

JS中第一种继承方案:原型继承(让子类的原型等于父类的实例即可)

function Parent () {
    this.x = 100;
}
Parent.prototype.getC = function getX () {
    return this.x;
}
function Child () {
    this.y=200;
}
Child.prototype = new Parent; //原型继承
Child.prototype.getY = function getY () {
    return this.y;
}
let c = new Child;
console.log(c.getC());

原型继承的特点:

  • 父类中私有和公有的属性和方法,最后都变为子类实例公有的
  • 原型继承并不会把父类的属性方法“拷贝”给子类,而是让子类实例基于__proto__原型链找到自己定义的属性和方法“指向

JS中第二种继承方式:call继承,只能继承父类中私有的,不能继承父类中公有的

function Parent () {
    this.x = 100;
}
Parent.prototype.getC = function getX () {
    return this.x;
}
function Child () {
    Parent.call(this);// 在子类函数中,把父类当做普通方法执行(没有父类实例,父类原型上的那些东西也就和他没关系了)
    //this.x=100相当于强制给c1这个实例设置了一个私有的属性x,属性值100,相当于让子类的实例继承了父类的私有的属性,并且也变为了子类私有的属性“拷贝式”
    this.y=200;
}
Child.prototype.getY=function getY(){
    return this.y;
}
let c1=new Child;
console.log(c1);

JS中第三中继承:寄生组合式继承(call继承+另类原型继承):

function Parent () {
    this.x=100;
}
Parent.protype.getX = function getX () {
    return this.x;
}
function Child () {
    Parent.call(this);
    this.y=200;
}
Child.prototype = Object.create(Parent.prototype); //创建一个空对象,让其原型指向该对象

Child.prototype.constructor = Child;

Child.prototype.getY = function getY () {
    return this.y;
}
let c1 = new Child;
console.log(c1);

ES6中继承写法:

class Parent{
    constructor(){
        this.x=100;
    }
    getX(){
        return this.x;
    }
}
//继承:extends Parent(类似于寄生组合继承),继承后一定要在constructor第一行加上super();
class Child extends Parent{
    constructor(){
        super();//类似于之前的call继承,super(100,200):相当于把Parent中的constructor执行,传递了100和200
        this.y=200;
    }
    getY(){
        return this.y;
    }
}
let c1=new Child;
console.log(c1);