JS 中的面向对象 prototype class

3,512 阅读7分钟

面向对象编程是将事物看成一个个对象,对象有自己的属性有自己的方法。

比如人,我们先定义一个对象模板,我们可以定义一些属性 比如,名字年龄和功能,比如走路。我们把这个叫做类。

然后帮们将具体数据传入模板,成为一个个具体的人,我们将它叫做实例。

JS 中面向对象是使用原型(prototype)实现的。

function Person(name, age) {
    this.name = name
    this.age = age
    this.walk = function(){}
}

Person.prototype.walk = function () {}

var bob = new Person('bob', 10)
console.log(bob.age)

其中的Person函数叫做构造函数,构造函数一般会将第一个字母大写, 构造函数创建特定类型的对象,构造函数中没有,显式的创建对象,和返回对象,直接将属性赋值给 this

我们使用new关键字创建对象实例,它会经历 4 个步骤,

  1. 创建一个新对象
  2. 将构造函数的的作用域赋给新对象
  3. 执行代码
  4. 返回新对象,实例会保存着一个 constructor 属性,该属性指向构造函数

我们也可以将walk函数写在构造函数中this.walk=function(){},但是这样写的话,每新建一个实例,实例都会新建一个walk函数,这样就浪费内存空间,我们将它放在prototype上这样就会让所有实例共享一个walk函数,但是如果都写了它会调用自己的walk函数而不是共享的。

每一个函数都有一个prototype属性,函数的prototype对象上的属性方法,所有实例都是共享的。

prototype对象有个constructor属性,它指向它的构造函数。

当创建一个实例时,实例内有会有个[[Prototype]]指针指向构造函数的原型对象,在浏览器中查看显示为__proto__属性。

当实例访问一个属性或者调用一个方法,比如bob.walk(),内部会首先在自身上查找这个方法,如果找到的话就完成,如果没有找到的话,就会沿着[[prototype]]向上查找,这就是为什么prototype上的方法都是共享,如果沿着[[prototype]]找到头,还没找到,那么就会报错bob.walk不是一个函数。

继承

继承主要是利用原型链,让子类的prototype等于父类的实例,也就是利用实例寻找属性和方法时,会沿着[[prototype]]向上找。

继承就是,一个子类继承父类的代码,而不用重新编写重复的代码。比如我们要写Cat, Dog等类,我们发现每个类都有类似this.name = name; this.age = age这些重复的代码,所以我们可以先写一个Animal类,让Cat,Dog继承这个类,我们就不用编写重复的属性和方法了。

function Animal(name) { this.name = name; this.age = 10 }
Animal.prototype.say = function () {
    console.log(this.name)
}
function Cat() { Animal.apply(this, arguments) }
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

我们用apply改变Catthis指向,让我们可以借用Animal的构造函数,然后再让Catprototype指向一个Animal实例,并把constructor修改正常。

如果我们初始化一个Cat类,然后调用say方法,那么在内部的查找流程是:

自身 -> 沿着[[prototype]]找到Cat.prototype(它是一个Animal实例)-> 沿着Animal实例的[[prototype]]查找 -> 找到Animal.prototype(找到run方法并调用)

我们发现Cat.prototype = new Animal()这样就会让Cat的prototype多出nameage两个属性。

function Animal(name) { this.name = name; this.age = 10 }
Animal.prototype.say = function () {
    console.log(this.name)
}
function Cat() { Animal.apply(this, arguments) }

function F(){}
F.prototype = Animal.prototype

Cat.prototype = new F()
Cat.prototype.constructor = Cat

我们使用了一个中间类函数F,让它的prototype等于父级的prototype,那么我们查找到F.prototype时,就自动到了Animal.prototype上。

我们如果想知道一个属性是不是属于自身而不是来自原型链则可以使用

实例.hasOwnProperty(属性) 查看该属性是否来自本身。

Object.getOwnPropertyNames(obj) 返回所有对象本身属性名数组,无论是否能枚举

属性 in 对象 判断能否通过该对象访问该属性,无论是在本身还是原型上

如果我们想获取一个对象的prototype,我们可以使用

Object.getPrototypeOf(obj) 方法,他返回对象的prototype

Object.setPrototypeOf(object, prototype) 方法,设置对象的prototype

还可以使用对象的__proto__属性获取和修改对象的prototype(不推荐)

属性描述符

在 js 中定义了只有内部才能用的特性,描述了属性的各种特性。

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

数据属性

  1. configurable 是否能配置此属性,为false时不能删除,而且再设置时会报错除了Writable
  2. enumerable 当且仅当该属性的enumerabletrue时,该属性才能够出现在对象的枚举属性中
  3. value 包含了此属性的值。
  4. writable 是否能修改属性值

存取描述符

  1. configurable
  2. enumerable
  3. get 读取时调用
  4. set 写入时调用

我们可以使用Object.defineProperty方法定义或修改一个对象属性的特性。

var obj = {}

Object.defineProperty(obj, "key", {
  enumerable: false, // 默认为 false
  configurable: false, // 默认为 false
  writable: false, // 默认为 false
  value: "static" // 默认为 undefined
});

Object.defineProperty(obj, 'k', {
    get: function () { // 默认为 undefined
        return '123'
    },
    set: function (v) {
        this.kk = v
    } // 默认为 undefined
})

使用Object.getOwnPropertyDescriptor可以一次定义多个属性

var obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
});

class

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

这样编写面向对象就更加的简单。

和类表达式一样,类声明体在严格模式下运行。构造函数是可选的。

类声明不可以提升(这与函数声明不同)。

class Person {
    age = 0 // 属性除了写在构造函数中也可以写在外面。
    static a = 0 // 静态属性

    constructor (name) { 
    // 构造函数,可选(如果没有显式定义,一个空的constructor方法会被默认添加)
        this.name = name
    } 
    
    // 类的内部所有定义的方法,都是不可枚举的
    say () { // 方法 共享函数
        return this.name
    }
    
    static walk() { // 静态方法
        
    }
}

typeof Person // "function"
Person === Person.prototype.constructor // true

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致,但是忘记加new会报错。

静态属性和静态方法,是属于类的,而不是属于实例的,要使用Person.walk()调用。

类的所有方法都定义在类的prototype属性上面。

// 上面等同于

Person.prototype = {
  constructor() {},
  say() {}
};
Person.a = 0
Person.walk = function () {}

ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

Class 内部调用new.target,返回当前 Class

与函数一样,类也可以使用表达式的形式定义。

const AA = class A {}
// 这个类的名字是A,但是A只在内部用,指代当前类。在外部,这个类只能用AA引用
const BB = class {}

let person = new class { // 立即执行的 Class
  constructor(name) {
    this.name = name;
  }
}('张三');

Class 继承

Class 可以通过extends关键字实现继承。

class Animal {
    constructor (name) {
        this.name = name
    }
}

class Cat extends Animal {
    constructor (...args) {
        super(...args) // 调用父类的 constructor 方法
                        // 必须调用且放在 constructor 最前面
    }
}

如果子类没有定义constructor方法,这个方法会被默认添加。

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

父类函数的静态属性和方法也会继承

super这个关键字,既可以当作函数使用,也可以当作对象使用。

super作为函数时,只能用在子类的构造函数之中,用在其他地方就会报错。

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

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

构造函数方法是不能继承原生对象的,

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

但是 class 可以继承。这样就可以构造自己的Array子类。

可以继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。