JS基础核心之原型

构造函数与实例

构造函数与普通函数的唯一区别:调用方式的不同,构造函数用new调用。

new的内部经历了什么?

  1. 创建一个Object对象
  2. 将构造函数的this指向这个对象
  3. 执行构造函数中的代码
  4. 返回Object对象

demo如下:

function Person () {
    // 1. 创建一个Obj
    var obj = {}
    // 2. this 指向内部创建的对象
    // this指向obj
    // 3. 执行构造函数中的代码
    console.log(666);
    // 4. 返回Obj对象
    /* return obj */
}
var p = new Person () // p为一个实例

构造函数的代码外在体现:改变this指向,返回一个对象。

用new关键字生成的对象称为实例。

原型

有了构造函数,就会有对应的原型对象。

每一个构造函数(或普通函数),都可以有一个prototype属性,该属性指向一个对象。这个对象,就是我们这里说的原型。

原型对象:

  • constructor: 指向构造函数
  • 原型对象中的属性和方法可以让所有对象实例共享
  • 原型对象的属性和方法只会初始化一次(构造函数中的属性和方法多次声明实例会触发多次调用)

如何获取原型对象:

  • 通过构造函数获取 构造函数.prototype
  • 通过实例获取:a.chrome的私有属性获取:proto;b.ES5标准方式获取:Object.getPrototypeOf
function Person () {
  console.log(this)
}
// 1.构造函数:构造函数的prototype指向 原型对象
console.log(Person.prototype)
var p1 = new Person ()
// 2. 实例:p1的[[prototype]]指向原型对象
// a. chrome私有属性__proto__
// b. ES5标准方式getPrototypeOf
console.log(p1.__proto__)
console.log(Object.getPrototypeOf(p1))

当需要添加多个原型方法的时候,建议重写原型对象:

function Person() {}

Person.prototype = {
    constructor: Person, // 重新指向构造函数
    getName: function() {},
    getAge: function() {},
    sayHello: function() {}
}

这种字面量的写法看上去简单很多,但是有一个需要特别注意的地方。Person.prototype = {}实际上是重新创建了一个{}对象并赋值给Person.prototype,这里的{}并不是最初的那个原型对象。因此它里面并不包含constructor属性。为了保证正确性,我们必须在新创建的{}对象中显示的设置constructor的指向。即上面的constructor: Person。

最后,面向对象铁三角:原型、构造函数、实例,它们三者之间的关系图如下:


原型、构造函数、实例三者关系图
  1. 创建: 创建构造函数的同时也创建了原型对象,new构造函数的时候创建了一个实例。
  2. 指向:
  • 构造函数与原型对象:构造函数prototype指向原型对象,原型对象constructor指向构造函数。
  • 实例与原型对象:实例[[prototype]]指向原型对象,原型对象没办法指向实例。
  • 实例与构造函数:实例与构造函数之间没有任何指向。但是实例可以间接通过原型对象的constructor方法指向构造函数。
  1. 每个函数(构造函数)都有 prototype 属性, 每个对象(实例)都有有[[prototype]]属性。每个原型对象有constructor属性。

判断

判断原型和实例的关系(返回布尔值) 判断实例与构造函数的关系

  • constructor: 一般用于判断该实例是否由某一构造函数生成

实例.constructor == Student //true

  • (重点)instanceof: 检测某个对象是不是某一构造函数的实例

实例 instanceof Student //true

  • (用的比较少)isPrototypeOf: 判断当前对象是否为实例的原型

原型对象.isPrototypeOf(实例) //true

原型的作用

原型的作用:共享相同的方法,节省内存分配空间

构造函数中使用方法很好用,但是单独使用存在一个浪费内存的问题(所有的属性/方法都写入实例中)。这样既不环保,也缺乏效率。

解决方案:构造函数+原型对象

  • 构造函数中添加属性
  • 原型对象中添加方法

结果:实例中的属性减少了,原型对象中的方法又能被所有的实例共享,最大限度的节省了内存。

原型链

实例到Object原型对象之间的链条称为原型链

原型模式的访问机制(原型搜索机制)

  • 读取实例对象的属性时,先从实例对象本身开始搜索。如果在实例中找到了这个属性,则返回该属性的值;
  • 如果没有找到,则继续搜索实例的原型对象,如果在原型对象中找到了这个属性,则返回该属性的值
  • 如果还是没找到,则向原型对象的原型对象查找,依此类推,直到Object的原型对象(最顶层对象);
  • 如果再Object的原型对象中还搜索不到,则抛出错误;

原型链原理图

[[prototype]】连接起来的路径,称为原型链。

图示如下:

原型链

继承

基于原型链的特征,我们可以很轻松的实现继承。

继承方法一:组合继承法

组合继承法的定义:

  • 继承属性:借用构造函数
  • 继承方法:原型链继承

组合继承法的缺点:

  • 多次执行父类构造函数
  • 在原型对象中生成多余的属性(尤其原型链继承方法的时候)

组合继承法的实现步骤:

  1. 借用构造函数继承属性
function Person(name,age,gender){
    this.name = name;
    this.age = age;
    this.gender = gender
}

// 学生
function Student(name,age,gender){
    // 继承属性
    // 借用构造函数法
    // 把Person中的this指向这里的this
    Person.call(this,name,age,gender);
    //new Person(name,age)
}

var s1 = new Student('刘备',50,'男');
console.log(s1)  // 刘备、50 、男
  1. 原型链继承

原型的继承,只需要将子级的原型对象设置为父级的一个实例,加入到原型链中即可。

function Person(name,age,gender){
}
Person.prototype.coding = function(){
    console.log('我在撸代码,我为自己代言');
}

Person.prototype.eat = function(){
    console.log('天王盖地虎,小鸡炖蘑菇');
}

// 学生
function Student(name,age,gender){
}

// 原型链继承法  子类的原型对象指向父类的实例。
// 只能继承方法
Student.prototype = new Person();

var s1 = new Student();
s1.coding(); // 我在撸代码,我为自己代言

子类的原型对象指向父类的实例,画出原理图如下(原型链继承):

原型链继承法

继承方法二:寄生组合继承法

目的/背景: 解决“组合继承法”的原型链继承中出现的冗余的属性的缺点。

寄生组合继承法的定义:

  • 继承属性:借用构造函数
  • 继承方法:原型式继承(代替原型链继承)

代码如下:

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

function Student(name, age, grade) {
    // 构造函数继承属性
    Person.call(this, name, age);
    this.grade = grade;
}

// 原型式继承  子类的原型对象指向父类的原型对象
Student.prototype = Object.create(Person.prototype, {
    // 不要忘了重新指定构造函数
    constructor: {
        value: Student
    },
    // 新增自己的方法
    getGrade: {
        value: function() {
            return this.grade
        }
    }
})


var s1 = new Student('ming', 22, 5);

console.log(s1.getName());  // ming
console.log(s1.getAge());   // 22
console.log(s1.getGrade()); // 5

原型式继承的原理:Object.create内部剖析

function create(proto, options) {
    // 创建一个空对象
    var obj = {};

    // 让这个新的空对象(实例)的原型对象 指向 父类的原型对象
    obj.__proto__ = proto;

    // 传入的方法都挂载到新对象上,新的对象将作为子类对象的原型
    // 记得重新指向子类的构造函数
    Object.defineProperties(obj, options);
    return obj;
}

组合继承法和寄生组合继承法,两者的本质区别:

  • 原型链继承,是通过子类的原型对象指向父类的实例
  • 原型式继承,是通过子类的原型对象指向父类的原型对象

画图理解:


原型式继承和原型链继承的区别

继承方法三:ES6继承法

ES6的继承, 底层是寄生组合继承法的封装。除了静态方法外。

ES6继承的定义:

  • super 继承属性
  • extends 继承原型链方法,实现原理是原型式继承 + 其他
// Class 关键字,类,可以理解为构造函数。
class Person {
  // constructor指向构造函数中的属性
  constructor (name, age) {
    this.name = name
    this.age = age
  }
  // 添加原型对象的方法
  eat () {
    console.log(this.name, '我是吃货')
  }
}
var p1 = new Person('height', 18)
p1.eat()
console.log(p1)


// extends 继承父类的原型方法
class Student extends Person {
  constructor (name, age) {
    // super 继承父类的属性
    super(name, age)
  }
}
var s1 = new Student('xing', 9)
console.log(s1)
s1.eat() 

补充:类的静态方法:

  • 静态方法方法不会被实例继承,而是直接通过类来调用Person.getInfo()
  • 父类的静态方法,可以被子类继承Man.getInfo()
class Person {
    constructor(){
        this.name = 'laoxie',
        this.age = 18;
    }
    static getInfo(){
      console.log(this.name)
    }
    say(){
        console.log(`Hello everyone, my name is ${this.name}, I'm ${this.age} years old`)
    }
}
class Man extends Person {
}
Man.getInfo()  // Man
Person.getInfo() // Person
var p1 = new Person()
var m1 = new Man()
p1.getInfo() // 报错:  p1.getInfo is not a function
m1.getInfo() // 报错:  m1.getInfo is not a function

extends的静态方法分析:

extends除了使用原型式继承,还使用了其他方法,让子类继承静态

function Person (age) {
  console.log('构造函数')
  this.age = age
}
Person.prototype.eat = function () {
  console.log('我是原型方法')
}
Person.say = function () {
  console.log('我是静态方法')
}
function Student () {
  Person.call(this)
}
Student.prototype = Object.create(Person.prototype, {
  constructor: {
    value: Student
  }
})
console.log(Student)
var s1 = new Student()
console.log(s1)
s1.eat()
s1.say() // 报错:找不到该函数, 可见extends除了使用原型式继承,还使用了其他方法,让子类继承静态。

注意属性/方法的位置:

我们还需要关注构造函数与原型的各自特性,有助于在创建对象时准确的判断我们的属性与方法到底是放在构造函数中还是放在原型中。如果没有理解清楚,这会给我们在实际开发中造成非常大的困扰。

比如:

// 模块内部
const a = 20;

function Person(name, age) {
  this.name = name;
  this.age = age;
  // 构造函数方法,每声明一个实例,都会重新创建一次,属于实例独有
  this.getName = function() {
    return this.name;
  }
}

// 原型方法,仅在原型创建时声明一次,属于所有实例共享
Person.prototype.getAge = function() {
  return this.age;
}

// 工具方法,直接挂载在构造函数名上,仅声明一次,无法直接访问实例内部属性与方法
Person.each = function() {}

结构图如下:

属性/方法的位置

参考资料

  • 老谢-第二阶段复习资料
  • 这波能反杀-详解面向对象、构造函数、原型与原型链
  • github-ES5/ES6 的继承除了写法以外还有什么区别?

你可能感兴趣的:(JS基础核心之原型)