【基础知识】原型对象

553 阅读13分钟

前言

  • 最近学习JavaScript中的原型这个概念,想要彻底了解下JavaScript是基于原型的编程语言这个说法。

  • 网上关于原型对象和原型链的文章也是一搜一大把,于是跟着大部队的思路,从构造函数这个概念入手,认认真真反复琢磨构造函数的prototype和对象的__proto__的关系,欢天喜地地认为:“我终于知道原型对象和原型链了,啥也不是事,哈哈哈!”

  • 但是,我的心还是剪不断啊理还乱,感觉这个构造函数也好,这个原型对象也罢,好像和Java(本人是先学习的Java,后接触的JavaScript中的构造函数)中的类的使用方式区别也不大呀,不就是定义一个构造函数(比如创建一个对象Student),然后在Student的原型对象上Student.prototype上定义属性或方法供new出来的实例(对象)使用(他们都可以调用父辈的方法),有啥不一样的?

function Student(name, sex, age, grade, hobby) {
    this.name = name;
    this.sex = sex;
    this.age = age;
    this.grade = grade;
    this.hobby = hobby;
}
Student.prototype.type = "person";
Student.prototype.play = function() {
    return this.name + " play games";
}
Student.prototype.read = function() {
    return this.name + " read books";
}
let Lucy = new Student("lucy", "女", "15", ["reading","travel","singing"]);
Lucy.play();
  • 看起来,除了构造函数是用function修饰的,调用方式不也一样一样的吗?我们在调用Student的过程中,一般也不会去再定义Lucy其他的属性,所以他们的区别是啥?

  • 原来我是被构造函数和new蒙蔽了双眼 其实newFunctionObject、函数的prototype这一些都是JavaScript模拟Java的语法,抛开这些,其实原型系统很简单(下面2条看不懂的话,可以先看完全篇再回过头来体会):

    A) 每个对象都有一个私有字段(隐藏属性)<prototype>:这个就是对象的原型

    B)读取一个属性,若对象本身没有,则会继续访问对象的原型,直到原型为空或找到为止。

  • 这里先提下个人 现在(目前为止) 对“基于原型对象” 和 “基于类” 的通俗理解:

基于原型对象:

1. 有一个对象 猫Cat
2. 根据猫Cat这个原型 创造一个新对象大猫“老虎”
3. 为大猫添加新的属性
4. 根据猫Cat, 创造其他猫科动物
5. 为其他动物添加他们特有的属性
6. 猫、大猫、其他猫科动物,他们都是一个对象
7. 所以原型对象是创建新对象的参照对象

基于类:

1. 有一个类 猫科动物 CatAnimal
2. 创建一个猫实例 cat
3. cat调用类的方法,不可动态添加新的属性和方法
4. 再次创建一个实例虎tiger
5. tiger和cat的所拥有的属性和方法是固有的,
6. 类CatAnimal描述的是他们的共有特征
7. 所以类是一个概念,是一类事务共有特征的抽象

JavaScript对象

在正式开始学习JavaScript中的原型对象之前,先来看看JavaScript中的对象及其特征。

JavaScript中的七种数据类型

numberstringbooleansymbolundefinednullobject(引用类型)。我们所见到的数组以及函数等,并不是独立的一种数据类型,而是包含在object里的。

JavaScript对象的特征

  1. 对象具有唯一标识性
let o1 = {name:"Lucy"}
let o2 = {name:"Lucy"}
o1 == 02 //false
  1. 对象具有状态
  2. 对象具有行为

对于第2点和第3点,不同的语言有不同的术语来表达

// Java: 状态=>属性,行为=> 方法
// JS中: 统一称为属性,下面的play也称为属性
let obj = {
    name : "Lucy",
    age: 20,
    play: function() {
        return "Lucy play basketball";
    }
}
  1. JavaScript独有的特色:可以动态改变属性(即动态修改状态和行为)

JS对象中的两种类型的属性

  1. 数据属性
数据属性包含四个特性,分别是:
configurable:能否被删除或被改变
enumerable:能否被遍历
writable:能否被赋值
value:属性值
    //定义数据类型一般方案
    let obj = {a : 1}  
    //定义数据类型方案2
    Object.defineProperty(obj,'b',{
      configurable:true,
      enumerable:true,
      writable:true,
      value:1
    })
  1. 访问器属性
访问器属性也包含四个特性,分别是:
configurable:能否被删除或被改变
enumerable:能否被遍历
getter:取值
setter:设置属性值
    let obj = {
        _age: 1, //下划线表示内部属性,
        //定义访问器属性的方案1
        get age(){
            return this._age
        },
        set age(nv){
            this._age = nv;
        }
    }
    console.log(obj.age) //1
    obj.age = 2;
    console.log(obj.age) //2
    
    //定义访问器属性的方案2
    Object.defineProperty(obj,'sex',{
      configurable:true,
      enumerable:true,
      get:function(){
        return this._sex;
      },
      set:function(newValue){
        this._sex = newValue;
      }
    })
  1. Object几个相关的方法掌握下
  • Object.defineProperty():通过描述对象,定义某个属性
  • Object.defineProperties():通过描述对象,定义多个属性
  • Object.getOwnPropertyDescriptor():获取某个属性的描述对象
  • Object.getOwnPropertyNames():遍历对象的属性

构造函数中的原型

虽然前言中说到“我被构造函数蒙蔽了双眼”,但是这块知识点还是很重要的,在es6之前,JS中的面向对象编程,一般都是通过构造函数来实现的吧。

prototype属性

构造函数有一个隐藏属性 prototype

对象有一个隐藏属性<prototype>,可能说成__proto__ 大家更为熟悉(这个属性才是 传说中的 原型对象)。

构造函数 constructor

  1. 构造函数的首字母必须大写,用来区分于普通函数

  2. 内部使用的this对象,来指向即将要生成的实例对象

  3. 使用new来生成实例对象

  4. 作用:实现面向对象编程

  5. 案例:

    /**
        name/age/nation/sayHello 都是明确定义的属性
        还有隐藏属性prototype和<prototype>(__proto__)
    */
    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.nation = "China";
        this.sayHello = function() {
            console.log("hello !");
        }
    }
    Person.prototype.play = function(){
        return "play games";
    }
    let lucy = new Person('Lucy',20);
  1. new 命令的原理? 阮一峰的面向对象编程
  • 创建一个空对象,作为将要返回的对象实例。
  • 将这个空对象的原型,指向构造函数的prototype属性。
  • 将这个空对象赋值给函数内部的this关键字。
  • 开始执行构造函数内部的代码。

对象与函数

  1. 对象类型
    lucy:具体的一个对象
    Person: 构造函数,也是一个对象
    Person中定义的属性也是对象(基本类型到底是对象吗?)
    (当然隐藏属性prototype也是对象)
    js中没有类和实例的说法
    只要是对象,就有<prototype>(__proto__)这个属性
    //打印下上面lucy和Person的__proto__
    console.log("lucy的__proto__")
    console.log(lucy.__proto__);
    console.log("Person的__proto__")
    console.log(Person.__proto__);

2. 基本类型到底是对象吗?

let obj = {
  name: "Lucy",
  age: 20
}
console.log(obj.name.__proto__)
console.log(obj.name.__proto__.__proto__)
console.log(obj.age.__proto__)
console.log(obj.age.__proto__.__proto__)

这里应该是转成了包装对象吧!!

对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper)。

所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。

var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);

typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"

v1 === 123 // false
v2 === 'abc' // false
v3 === true // false

上面代码中,基于原始类型的值,生成了三个对应的包装对象。可以看到,v1、v2、v3都是对象,且与对应的简单类型值不相等。

包装对象的设计目的,首先是使得“对象”这种类型可以覆盖 JavaScript 所有的值,整门语言有一个通用的数据模型,其次是使得原始类型的值也有办法调用自己的方法。

  1. 函数:被function修饰,数据类型为Function
    /**
    上面提到的原型对象prototyp这个属性,只有函数对象才有;
    除了构造函数,普通函数也有
    我们主要了解构造函数有这个即可!!!
    */
    function test(){
    	return "test";
    }
    console.log(test.prototype)
    //而test.prototype也是一个对象,所以它有__proto__属性

    //打印下上面构造函数Person的prototype属性
    console.log(Person.prototype)

原型对象概述

这一节的知识点拷贝自: 阮一峰的原型对象

构造函数的缺点

JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。

function Cat (name, color) {
  this.name = name;
  this.color = color;
}
var cat1 = new Cat('大毛', '白色');
cat1.name // '大毛'
cat1.color // '白色'

上面代码中,Cat函数是一个构造函数,函数内部定义了name属性和color属性,所有实例对象(上例是cat1)都会生成这两个属性,即这两个属性会定义在实例对象上面。

通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。

function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.meow = function () {
    console.log('喵喵');
  };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.meow === cat2.meow
// false

上面代码中,cat1cat2是同一个构造函数的两个实例,它们都具有meow方法。由于meow方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow方法。这既没有必要,又浪费系统资源,因为所有meow方法都是同样的行为,完全应该共享。

这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。

prototype 属性的作用

JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

下面,先看怎么为对象指定原型。

JavaScript规定,每个函数都有一个prototype属性,指向一个对象。

    function f() {}
    typeof f.prototype // "object"

上面代码中,函数f默认具有prototype属性,指向一个对象。

对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。

function Animal(name) {
  this.name = name;
}
Animal.prototype.color = 'white';
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
cat1.color // 'white'
cat2.color // 'white'

上面代码中,构造函数Animalprototype属性,就是实例对象cat1cat2的原型对象。原型对象上添加一个color属性,结果,实例对象都共享了该属性。

原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。

Animal.prototype.color = 'yellow';
cat1.color // "yellow"
cat2.color // "yellow"

上面代码中,原型对象的color属性的值变为yellow,两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。

如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。

cat1.color = 'black';
cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';

上面代码中,实例对象cat1color属性改为black,就使得它不再去原型对象读取color属性,后者的值依然为yellow

总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。

Animal.prototype.walk = function () {
  console.log(this.name + ' is walking');
};

上面代码中,Animal.prototype对象上面定义了一个walk方法,这个方法将可以在所有Animal实例对象上面调用。

原型链

对象与函数

简介

对象链

    let foo = new Foo(); 
    /**
    foo是一个对象,它有__proto__属性
    且该属性指向构造函数Foo的原型属性prototype
    */
    foo.__proto__ == Foo.prototype
    /**
    原型对象Foo.prototype也是一个对象,
    它的__proto属性指向Object构造函数的prototype
    */
    Foo.prototype.__proto__ == Object.prototype
    /**
    那原型对象Object.prototype自身也是一个对象呀
    那Object.prototype的__proto__指向谁呢?
    */
    Object.prototype.__proto__ == null
    //所以从对象这条线来看,他们的关系是
    foo.__proto__.__proto__.__proto__ == null

函数链

/**
构造函数Foo也是对象,
它的__proto__又指向谁呢?
*/
Foo.__proto__ = Function.prototype
//所以,没有继承等关系的构造函数的
//__proto__都指向Function.prototype

/**
Function是个特殊的构造函数吧!
和之前的Foo.prototype.__proto__结果一致
*/
Function.prototype.__proto__  == Object.prototype
/**
哈哈哈,又到这里了。
Object当然也是一个特殊的构造函数了
*/
Object.prototype__proto__ == null
/**
那,那那... 
Function和Object的__proto__又都指向哪里呢?
说了,他们俩不过是特殊的构造函数,
其实和Foo也没多大差别是吧
所以...
*/
Function.__proto__ == Function.prototype
Object.__proto__ == Function.prototype
//而Function又只是一个稍微特殊的对象
Function.prototype.__proto__  == Object.prototype
//所以从函数对象这条线来看,他们的关系是
Foo.__proto__.__proto__.__proto__ == null

经典图

(网上流传的经典图,本人是上面的关系理清了来看这张图才看的懂)

总结

  • __proto__:在JavaScript中,每个对象都拥有一个原型对象,而指向该原型对象的内部指针则是 __proto__,通过它可以从中继承原型对象的属性,原型是JavaScript中的基因链接,有了这个,才能知道这个对象的祖祖辈辈。从对象中的__proto__可以访问到他所继承的原型对象。
  • prototype:函数对象除了拥有__proto__属性之外,还拥有prototype属性。通过该函数构造的新的实例对象,其原型指针__proto__会指向该函数的prototype属性。而函数的prototype属性,本身是一个由Object构造的实例对象。
  • 原型链:‘对象实例’通过指针__proto__指向原型对象prototype形成的那个链条关系

对象的继承

直接看这里吧JavaScript常用八种继承方案

ES6中的类与对象的创建

Class类

Class 的基本语法

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

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

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

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

基本上,ES6class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6class改写,就是下面这样。

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

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

ES6直接访问操纵原型对象

Object.setPrototypeOf():设置指定对象的原型

Object.getPrototypeOf():返回指定对象的原型

Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction();

学习链接

JavaScript面向对象简介

廖雪峰的面向对象编程

阮一峰的对象封装

阮一峰的原型对象

三张图搞懂JS的原型对象与原型链。重点推荐

JavaScript常用八种继承方案