JS

作者: shaokang 时间: February 23, 2022字数:4912

继承

1. 原型链继承

function Person(name) {
    this.name = name;
}

Person.prototype.getName = function() {
    return this.name;
}

function Student() {
}

Student.prototype = new Person();

var student = new Student();
student.getName();

缺点:父类构造函数中的引用类型会被所有子类实例共享。

2. 构造函数继承

function Person(name) {
    this.name = name;
}

Person.prototype.getName = function() {
    return this.name;
}

function Student(name, age) {
    Person.call(this, name);
    this.age = age;
}

var student = new Student();
student.age;

缺点:只能继承父类的实例属性和方法,不能继承原型属性和方法;必须在父类构造函数中定义方法,无法实现函数复用,构造函数中的方法会在子类创建时被重新创建(副本),影响性能。

3. 组合继承

function Person(name) {
    this.name = name;
}

Person.prototype.getName = function() {
    return this.name;
}

function Student(name, age) {
    Person.call(this, name);
    this.age = age;
}

Student.prototype = new Person();
// 需要重新设置子类的 constructor,Student.prototype = new Parent() 相当于子类的原型对象完全被覆盖了
Student.prototype.constructor = Student;
// 在原型指向后再定义子类的原型方法
Student.prototype.getAge = function () {
    return this.age;
}

var student = new Student();
student.getAge();
student.constructor // Student

优点:父类构造函数始终会被调用两次,一次在是创建子类原型时调用,另一次是在子类构造函数中调用;子类创建实例时,原型中会存在两份相同的属性和方法

4. 寄生组合继承

function Person(name) {
    this.name = name;
}

Person.prototype.getName = function() {
    return this.name;
}

function Student(name, age) {
    Person.call(this, name);
    this.age = age;
}

inherit(Student, Person);

function inherit(child, parent) {
  var prototype = object(parent.prototype)
  prototype.constructor = child
  child.prototype = prototype
}

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

重点在 inherit。相当于

function Person(name) {
    this.name = name;
}

Person.prototype.getName = function() {
    return this.name;
}

function Student(name, age) {
    Person.call(this, name);
    this.age = age;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

之所以用 Object.create,是为了获取一份父类原型的拷贝,不影响父类原型的构造函数。

4. ES6 继承

extends 实现继承

原型

所有引用类型(函数,数组,对象)都拥有proto属性(隐式原型) 所有函数除了有proto属性之外还拥有 prototype 属性(显式原型) 原型对象:每创建一个函数,该函数会自动带有一个 prototype 属性,该属性是一个指针,指向了一个对象,我们称之为原型对象。

原型链

原型对象的 constructor 指向构造函数,实例对象的 proto 指向原型对象。实例对象的属性会随着原型对象一直往上找,直到最顶层。

作用域

定义变量的有效范围。作用域包含全局作用域、函数作用域、块级作用域。

作用域链

当前作用域不存在变量就去父级作用域查找,依次向上寻找,直到访问 window 结束。

闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途;
● 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
● 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

执行上下文

创建执行上下文有两个阶段:创建阶段和执行阶段
1)创建阶段
(1)this 绑定
● 在全局执行上下文中,this 指向全局对象(window 对象)
● 在函数执行上下文中,this 指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创建词法环境组件
● 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
● 词法环境的内部有两个组件 - 环境记录器 : 用来储存变量个函数声明的实际位置 - 外部环境的引用:可以访问父级作用域

(3)创建变量环境组件
● 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

2)执行阶段
此阶段会完成对变量的分配,最后执行完代码。

执行上下文类型

(1)全局执行上下文
任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的 window 对象,并且设置 this 的值等于这个全局对象,一个程序中只有一个全局执行上下文。

(2)函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。

(3)eval 函数执行上下文
执行在 eval 函数中的代码会有属于他自己的执行上下文,不过 eval 函数不常使用,不做介绍

作用域和执行上下文的区别

执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。

作用域、词法环境、执行上下文的概念

作用域:独立的区域,让变量不会向外暴露出去。作用域最大的用处就是隔离变量。内层作用域可以访问外层作用域。一个作用域下可能包含若干个执行上下文。

词法环境:相应代码块内标识符与变量值、函数值之间的关联关系的一种体现。词环境内部包含环境记录器和对外部环境的引用。环境记录器是存储变量和函数声明的实际位置,对外部环境的引用意味着可以访问父级词法环境。

执行上下文:JavaScript 代码运行的环境,创建执行上下文时会进行 this 绑定、创建词法环境和变量环境。

隐式类型转换

JavaScript 中每个值隐含的自带的方法,用来将值转换为基本类型值。如果值为基本类型,则直接返回值本身;

如果值为对象:

ToPrimitive(obj, type)

type 的值为 number 或者 string。

(1)当 type 为 number 时规则如下:

  • 调用 obj 的 valueOf 方法,如果为原始值,则返回,否则下一步;
  • 调用 obj 的 toString 方法,后续同上;
  • 抛出 TypeError 异常。

(2)当 type 为 string 时规则如下:

  • 调用 obj 的 toString 方法,如果为原始值,则返回,否则下一步;
  • 调用 obj 的 valueOf 方法,后续同上;
  • 抛出 TypeError 异常。

可以看出两者的主要区别在于调用 toString 和 valueOf 的先后顺序。默认情况下:

  • 如果对象为 Date 对象,则 type 默认为 string;
  • 其他情况下,type 默认为 number。

运算符之间的隐式转换

  1. 加号运算符

    如果两边至少一个 sting 类型,两边都会转换为字符串;其他情况下,都会转换为数字。

  2. -、*、\ 操作符

    都转换为数字

  3. == 操作符

    null == undefined 比较结果是 true,除此之外,null、undefined 和其他任何结果的比较值都为 false。 两边值尽可能转换为数字比较,详见 MDN

  4. <、> 操作符

    如果两边都是字符串,则按照字典顺序比较;其他情况下,两边都会转换为数字比较。

事件循环

浏览器事件循环

Javascript 单线程任务被分为同步任务和异步任务。异步任务一种是宏任务(MacroTask)也叫 Task,一种叫微任务(MicroTask)。每次单个宏任务执行完毕后,检查微任务队列是否为空,如果不为空,会按照先入先出的规则全部执行完微任务后,清空微任务队列, 然后再执行下一个宏任务。

node 事件循环

基于 Libuv 实现。node 调用栈执行完后会执行异步队列(nextTick -> 微任务队列),执行完后进入事件循环。
首先判断 timer 队列是否有回调,后续进入 poll 轮训执行队列,如果存在事件任务则执行。如果不存在,会其他队列是否存在事件。比如检查 check 队列,执行 setImmediate 回调。若不存在则一直等待 i/o 事件回调。最后执行 close 的回调队列。
https://www.bilibili.com/video/BV13A4y1Q7N5/?spm_id_from=333.788

常见知识点

  1. Object.create(null) 和 {} 的区别?
    使用 Object.create(null) 创建的对象,没有任何属性,不会继承原型链上的属性。{} 相反。Object.create(null) 得到一个非常干净且可定制化的对象,能够节省 hasOwnProperty 和 for..in 带来的性能损失。

  2. for in 和 Objects.keys() 的区别?
    for in 一般用于遍历对象属性。会遍历自身可枚举的属性以及原型链上可枚举的属性。Object.keys 只会遍历自身可枚举的属性。

  3. hasOwnProperty 和 in 的区别?
    两者都能检查队形中是否存在指定属性。in 能检查实例属性和原型上的属性,hasOwnProperty 只能检查实例上的属性。

  4. 尾递归
    函数尾部调用自身。好处:只需要保存调用栈,复杂度为 O(1)。 普通递归需要用栈保存每一层的返回点、局部变量,调用过多会造成栈溢出,复杂度为 O(n)。