Skip to content

JavaScript知识点整理

Nina.wang edited this page Nov 20, 2020 · 15 revisions

内容链接

  1. 继承以及优缺点整理
  2. Class
  3. Promise

继承以及优缺点整理

本章节对应demo练习 在学习继承的相关知识点之前,先整理一下创建对象的几种模式以及它们的优缺点

创建对象模式以及优缺点整理

背景:最初是使用Object构造函数单个字面量创建单个对象。 缺点:使用同一个接口创建多个对象,会产生大量重复的代码。 为了解决这个问题,使用了工厂模式的一种变体。

工厂模式

考虑到ECMAScript中无法创建类,就使用函数来封装以特定接口创建对象的细节。

function createPerson(name, age, job) {
    var o = Object.create(null)
    o.name = name
    o.age = age
    o.job = job

    o.sayName = function () {
        console.log(this.name)
    }
    return o
}

var person1 = createPerson('test1', '18', 'student')
var person2 = createPerson('nina', '2*', 'web developer')

解决了上述问题,无论调用多少次,只返回包含三个属性的对象;缺点:创建的对象原型指向null,无法识别对象。

构造函数

function Person() {
    this.name = 'Tylor'
    this.songs = ['You Belong With me']
    this.sayName = function () {
        console.log(this.name)
    }
}

function Child() {
    Person.call(this)
}
var child0 = new Child()
console.log(Object.getPrototypeOf(child0) === Child.prototype) // true
var child1 = new Child()
child1.songs.push('What')
var child2 = new Child()
console.log(child2.songs) // ['You Belong With me']

使用构造函数创建实例,必须用new操作符,意味着可以将它的实例标识为一种特定的类型。缺点:每个方法都要在每个实例上创建一遍,若将函数提到全局,可解决这个问题,但又会污染全局作用域。因为该函数实际只能被某个对象调用。

原型模式

function Person() {
}

var friend = new Person()
Person.prototype.sayHi = function () {
    console.log('hello')
}

Person.prototype = {
    // constructor: Person,
    name: 'prototype',
    friends: ['a', 'b'],
    sayName: function () {
        return this.name
    }
}

var person1 = new Person()
var person2 = new Person()
person1.friends.push('c')

console.log(person1.sayName === person2.sayName) // true
console.log(person1.friends === person2.friends) // true
console.log(person1.constructor == Object) // true
console.log(friend.constructor == Person) // true

console.log(friend.sayHi()) // 'hello'
console.log(friend.sayName) // undefined

特点:所有对象实例共享它所包含的属性和方法,这即是原型模式的优点也是它的缺点,对于引用类型的属性来说,这个问题比较突出。 需要注意的是:如果设置构造函数的prototype为对象字面量形式创建的新对象,那么constructor将不再指向该构造函数,并且会切断了现有原型与任何之前已存在的对象实例之间的关系。

constructor__proto__prototype

开始继承前,还要再捋捋这三个到底是啥。

原型对象

只要创建了一个新函数,就会(根据一组特定的规则)为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有的原型都会自动获得一个contructor属性,这个属性包含一个指向prototype属性所在函数的指针。当调用构造函数创建一个新实例,新实例中会包含一个(__proto__)指向构造函数的原型对象。

再总结一下:prototype是(构造)函数的一个属性,指向函数的原型对象constructor原型对象默认属性,指向(构造)函数__proto__构造函数实例的属性,指向构造函数的原型对象

重写原型对象

假设创建了一个Person构造函数,如果将Person.prototype设置为等于一个以字面量创建的对象,那么constructor就不再指向Person了,而是指向Object(也就是等于以字面量创建对象的constructor了,重新赋值了嘛。);如果constructor很重要的话,可以重新设置其值为Person。重写原型对象还会切断现有原型与之前已存在实例之间的联系;已存在实例引用的仍然是最初的原型。

继承

许多OO语言支持两种继承方式:接口继承和实现继承。ECMAscript只支持实现继承,其实现继承主要是依靠原型链来实现。

原型链继承

基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

function SuperType() {
    this.property = '111'
    this.color = ['violet', 'orange']
}

function SubType() {
}

SubType.prototype = new SuperType()

var instance1 = new SubType()
instance1.property // '111'
// 但是问题如下:
instance1.color.push('red')

var instance2 = new SubType()
instance2.color // ['violet', 'orange', 'red']

缺点:1.包含引用类型的原型,原型属性会被所有实例共享;2.实例不能向超类传参,也就是说不能在不影响所有对象实例的情况下,给超类型的构造函数传参。

借用构造函数(经典继承)

function SuperType(name) {
    this.name = name
    this.color = ['violet', 'orange']
}

function SubType(name) {
    SuperType.call(this, name)
}

var instance1 = new SubType()
instance1.color.push('white')

var instance2 = new SubType(passenger)
instance2.color // ['violet', 'orange']
//可传参
instance2.name //'passenger'

// 问题是:
SuperType.prototype.age = '18'
SuperType.prototype.sayName = function() {
    console.log(this.name)
}

var instance3 = new SubType()
instance3.age   //undefined
instance3.sayName //undefined

优点:每个实例都会具有自己的属性;实例可向超类型传参。

缺点:实例的方法无法复用;超类型的原型中的属性和方法,子类型无法继承。

组合继承(最常用的继承模式)

function SuperType(name) {
    this.name = name
    this.color = ['violet', 'orange']
}

SuperType.prototype.age = '18'
SuperType.prototype.sayName = function() {
    console.log(this.name)
}

function SubType(name, job) {
    SuperType.call(this, name) // 第二次调用
    this.job = job
}

SubType.prototype = new SuperType()  // 第一次调用
// 组合继承的问题(创建了不必要的属性,通过构造函数覆盖该属性)
console.log(SubType.prototype.color) // [ 'violet', 'orange' ]
SubType.prototype.constructor = SubType
SubType.prototype.sayJob = function() {
    console.log(this.job)
}

var instance1 = new SubType('test1', 'student')
instance1.color.push('blue')
instance1.sayName()  // 'test1'

var instance2 = new SubType('nina', 'web developer')
instance2.color //['violet', 'orange']
instance2.sayJob() //'web developer'

组合继承避免了原型链和借用构造函数的缺点,融合了两者的优点,成为Javascript中最常用的继承模式。

缺点:无论什么情况,都需要调用两次超类构造函数;不得不在调用子类型构造函数中重写对象实例的属性。

寄生组合式继承

通过借用构造函数继承属性,通过原型链的混成形式继承方法。解决了组合继承的问题。

function SuperType(name) {
    this.name = name
    this.color = ['violet', 'orange']
}
SuperType.prototype.age = '18'
SuperType.prototype.sayName = function() {
    return this.name
}
function SubType(name, job) {
    SuperType.call(this, name)
    this.job = job
}
// 留意
function F() {}
F.prototype = SuperType.prototype;
SubType.prototype = new F()
SubType.prototype.constructor = SubType
SubType.prototype.sayJob = function() {
    return this.job
}

该方式只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上创建不必要的、多余的属性。所以普片认为寄生组合式继承是最理性的继承范式。

回到顶部

class

ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类实际上是个“特殊的函数”,类语法有两个组成部分:类表达式和类声明。

基本语法

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    toString() {
        return `(${this.x},${this.y})`;
    }
}
  • 其中constructor就是构造方法,this关键字代表实例对象。
  • 类的所有方法都是定义在类的prototype属性上的。
// 等同于
Point.prototype = {
    constructor(){},
    toString() {}
}
  • 但是类内部定义的所有方法都是不可枚举的。(这一点与ES5)行为不一致。
Object.keys(Point.prototype);  // []

constructor

一个类必须有constructor方法,若是没有显示定义,一个空的constructor方法会被默认添加。constructor方法默认返回实例对象this,也可以指定返回其他对象。若果返回新对象,那么实例的对象将不是(Foo)类的实例。

class Foo {
    constructor() {
        return Object.create(null);
    }
}

new Foo() instanceof Foo; // false

类必须使用new调用,否则会报错。

getter and setter

与ES5写法一致,并且都是设置在属性的描述符对象上的。

class Point {
    constructor() {
    }

    set prop(val) {
        console.log('setter:' + val);
    }

    get prop() {
        console.log('getter');
    }
}
const instance = new Point();
instance.prop = 123; // setter:123
instance.prop; // getter
const descriptor = Object.getOwnPropertyDescriptor(Point.prototype, 'prop');
console.log('get' in descriptor);
console.log('set' in descriptor);

注意点

  1. 类和模块内部默认就是严格模式;
  2. 不存在提升。(为了保证在继承的时候,子类在父类之后定义。)
  3. name属性,本质上ES6的类知识ES5构造函数的一层包装,所以函数的许多特性都被class继承,包括name
  4. Generator方法,如果在某个方法的前面加上(*) ,就表示该方法是Generator方法。
  5. this的指向,类的方法内部如果包含this, 它默认指向类的实例。如果要单独使用内部有this的方法,则需手动将 this绑定到类的实例上,否则很可能报错。因为类内部使用的是严格模式,所以this实际上指向的是undefined

静态方法

如果一个方法前加上了static关键字,则该方法不会被实例继承,而是直接通过类调用。静态方法内部的this指向类而不是实例。父类的静态方法,可以被子类继承。并且静态方法也是可以从super对象上调用。

class Foo {
    static bar() {
      this.baz();
    }
    static baz() {
      console.log('hello');
    }
    baz() {
      console.log('world');
    }
  }
  
Foo.bar();// hello

实例属性的新写法

实例属性可以直接定义在类的最顶层。

class Foo {
    _count = 0;
    ...
}

new.target属性

new.target返回new作用于的那个构造函数的名称。需要注意的是子类继承父类时,new.target返回的是子类的名称。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确

Class继承

Class通过extends关键字,继承父类所有的属性和方法。子类constructor中需调用super方法,否则在创建实例的时候会抛错。如果子类不调用super方法,就得不到this对象。 与ES5不同, ES6继承机制是先将父类的实例对象的属性和方法,添加到this上(所以必须先调用super方法),然后用子类的构造函数修改this。 只有super可以调用父类实例。在子类构造函数中,只有调用了super之后,才可以使用this关键字。

super

super既可以当作函数使用,又可以当作对象使用。

  • super作为函数调用时,代表父类的构造函数。但返回的是子类的实例

  • super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

  • super()作为函数时,只能用在子类的构造函数之中。

  • 由于super在普通方法中指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

  • 普通方法通过super调用父类方法时,方法内部的this指向当前子类的实例。

  • super在静态方法中指向父类,但通过super调用父类方法时,方法内部的this指向当前的子类。 而不是子类的实例。

  • 注意super必须显示指定作为函数或者对象使用,否则会报错。

class A {
  constructor() {
    this.x = 1;
    this.test = 'test';
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
     console.log(super.test);
     super.print();
  }
}
B.x = 3;
B.m();
//  undefined
// 3

回到顶部

Promise

Promise是异步编程的一种解决方案,比传统的解决方案——回调和事件——更合理和强大。Promise总共有三种状态:pendingfulfilledrejected;只有异步操作的结果可以决定当前处于那种状态,其余手段无法改变。状态只会由pending转变为fulfilled或者由pending转变为rejected,状态一旦转变就不会再变了,并且会一直保持这个结果,这是就成为resolved(已定型)。

Promise的缺点:

  • 无法取消,一旦新建就会立即执行,无法中途取消;
  • 如果不设置回调,promise内部抛的错误不会反应到外部
  • 当处于pending时,无法得知目前进行到哪个阶段

基本用法

ES6规定,Promise是一个构造函数,用来生产Promise实例。

回到顶部