JavaScript之原型的理解

在 JavaScript 中,一切皆对象。对象也分为普通对象和函数对象。 o1、o2、o3 是普通对象,f1、f2、f3 是函数对象。 区别 :通过 new Function() 创建出来的对象都是函数对象,f1、f2 归根到底也是通过 n

目录
  1. 普通对象和函数对象
    1. 代码
    2. 分析
  2. 构造函数
    1. 代码
    2. 分析
  3. 原型对象
    1. 代码
    2. 分析
  4. 继承
    1. 代码
    2. 分析
  5. 隐式原型
    1. 代码
    2. 分析
  6. 构造器
    1. 代码
    2. 分析
  7. 原型链
    1. 代码
    2. 分析
  8. 原型链继承
    1. 代码分析
    2. 分析
  9. 继承改变
    1. 代码
    2. 分析
  10. hasOwnProperty
    1. 代码
    2. 分析
  11. ES6 的类
    1. 代码
    2. 分析
  12. 总结

普通对象和函数对象#

在 JavaScript 中,一切皆对象。对象也分为普通对象和函数对象。

代码#

var o1 = {};
var o2 = new Object();
var o3 = new f1();
 
function f1() {}
var f2 = function () {};
var f3 = new Function();
 
console.log(typeof o1); // object
console.log(typeof o2); // object
console.log(typeof o3); // object
 
console.log(typeof f1); // function
console.log(typeof f2); // function
console.log(typeof f3); // function

分析#

o1、o2、o3 是普通对象,f1、f2、f3 是函数对象。 区别:通过new Function()创建出来的对象都是函数对象,f1、f2 归根到底也是通过new Function()创建出来的。

构造函数#

对象归根到底都是通过函数创建出来的,是通过构造函数创建出来的。

代码#

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    alert(this.name);
  };
}
 
person1 = new Person("Hale", 28, "engineer");
person2 = new Person("Bill", 26, "teacher");

分析#

构造函数:任何通过 new 操作符来调用的函数,都可以称之构造函数,如 Person。

实例对象:通过构造函数创建的对象叫做实例对象,如 person1、person2。

原型对象#

任何函数都有一个 prototype 属性,它指向一个对象,这个对象就是原型对象。

代码#

function Person() {}
Person.prototype.name = "Hale";
Person.prototype.age = 28;
Person.prototype.job = "engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};
 
person1 = new Person();
person1.sayName(); //  'Hale';
 
person2 = new Person();
person2.sayName(); //  'Hale';
 
console.log(person1.sayName === person2.sayName); // true

分析#

什么是原型对象?

Person.prototype就是函数 Person 的原型对象,它其实就是一个普通对象。

这个对象有三个属性(name,age,jon)和一个方法(sayName())。

原型对象有什么用?

主要作用是用来继承的,原型对象可以让特定类型的所有实例共享属性和方法。

继承#

代码#

function Person(name) {
  this.name = name;
}
 
Person.prototype.job = "engineer";
 
Person.prototype.sayName = function () {
  alert(this.name);
};
 
person1 = new Person("Hale");
console.log(person1.job); // engineer
person1.sayName(); // 'hale'
 
person2 = new Person("Amy");
console.log(person2.job); // engineer
person2.sayName(); // 'Amy'

分析#

sayName是构造函数Person原型对象的方法。

通过构造函数创建的两个实例对象person1person2虽然名字不一样,但是他们都继承了构造函数原型链的属性job和方法sayName

隐式原型#

在最新的 ES 语法中,每一个对象都有一个__proto__属性,这个属性也是原型属性,但是和prototype有所区别,这个属性叫做“隐式原型”。为了区分,prototype叫做“显示原型”。

代码#

var f1 = new Function(); // 函数对象
var o1 = new Object(); // 普通对象
 
console.log(f1.__proto__ === Function.prototype); // true
console.log(o1.__proto__ === Object.prototype); // true

分析#

通过构造函数生成的对象的__proto__(隐式原型)都指向它的构造函数的prototype(显示原型)。

构造器#

通过构造函数生成的对象都有一个constructor属性,这个属性叫做构造器。

代码#

var f1 = new Function(); // 函数对象
var o1 = new Object(); // 普通对象
 
console.log(f1.constructor === Function); // true
console.log(f1.__proto__.constructor === Function); // true
 
console.log(o1.constructor === Object); // true
console.log(o1.__proto__.constructor === Object); // true

分析#

通过构造函数生成的实例对象的构造器指向它的构造函数,对象的隐性原型的构造器也指向它的构造函数。

原型链#

JavaScript 一切皆对象,我们还知道每一个对象都有隐式原型属性,指向它的构造函数的原型对象。那么构造函数的原型对象也是一个对象,那么它的隐式原型属性指向谁呢?

代码#

function Foo() {}
var f = new Foo();
 
console.log(f.__proto__ === Foo.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true

分析#

我们发现构造函数的原型对象的隐式原型指向的是内置函数Object的原型对象。

如果再往上一层,又指向哪个呢?它的尽头在哪里?

image.png

如上图所示,通过__proto__属性,层层向上,就形成了一条“链子”,这条“链子”就是原型链,它的尽头是null

原型链的形成是靠隐式原型__proto__属性的,而非显示原型prototype属性。当 JS 引擎执行对象的属性或者方法时,先查找对象本身是否存在该属性或者方法,如果不存在,会在原型链上的__proto__中,一级一级的往上查找,直到找到原型链的尽头null为止。

原型链继承#

代码分析#

function Animal() {}
Animal.prototype.sayName = function () {
  alert("Animal");
};
function Dog() {}
Dog.__proto__ = Animal.prototype;
Dog.prototype.sayName = function () {
  alert("Dog");
};
var dog = new Dog();
 
console.log(dog.sayName()); // Dog
console.log(Dog.sayName()); // Animal
console.log(Animal.sayName()); // 报错

分析#

疑问:Dog.sayName()输出的结果为什么是Animal而不是Dog原型对象的方法中的Dog呢?

自己本身原型对象上就有方法,为什么还要用别的方法呢?

那是因为函数的prototype对象的作用只是提供(子类)继承使用的,本身无法使用 prototype 的属性和方法,用了会报错。

函数对象上的prototype不属于自身的原型链,它只是创建子类的核心,决定了继承的子类的数据类型,它在继承的子类的原型链上。而对象的__proto__才是记录着自己的原型链,决定了自身的数据类型,改变__proto__就等于改变对象的数据类型。

原型链的形成真正是靠__proto__而非prototype,是靠__proto__来“连接”的,“连接”的对象是“父类”的prototype

现在回头看看原型对象的继承,其实也就是原型链的继承,只不过它是原型链继承的一小段而已。

继承改变#

代码#

function fn() {}
console.log(fn.__proto__ === Function.prototype); // true
console.log(fn instanceof Function); // true
console.log(fn instanceof Array); // false
 
fn.__proto__ = Array.prototype; // 改变__proto__指向
console.log(fn.__proto__ === Function.prototype); // false
console.log(fn instanceof Function); // false
console.log(fn instanceof Array); // true

分析#

instanceof方法是用来检测对象的prototype是否存在于另外一个对象的原型链上。

JavaScript 内置的Function对象是专门用来创建各种函数的,所以所有函数的__proto__都指向Functionprototype的,而Array函数是用来创建数组的。

fn 本来是一个函数,它的__proto__指向的是Functionprototype,继承的是Functionprototype上的属性和方法,后面改变函数fn__proto__指向,让它指向了函数Arrayprototype,这样 fn 函数现在继承的是Array函数的prototype上的属性和方法。

这相当于直接改变了它的数据类型,由函数直接改成了数组,现在你可以测试下函数 fn 是否可以使用Arrayprototype上的属性和方法。

hasOwnProperty#

私有属性和通过原型链继承的属性要怎么区分呢?通过hasOwnProperty方法来区分。

代码#

function Foo(name, age, job) {
  // 私有属性和方法
  this.name = name;
  this.age = age;
  this.job = job;
}
 
// 原型链的属性和方法
Foo.prototype.city = "Shenzhen";
Foo.prototype.sayName = function () {
  alert(this.name);
};
f = new Foo("hale", 28, "engineer");
 
for (key in f) {
  console.log(key, f[key]); // 打印所有的属性和方法
}
for (key in f) {
  if (f.hasOwnProperty(key)) {
    console.log(key, f[key]); // 只打印私有属性和方法
  }
}

分析#

使用for ... in可以把对象的所有的属性都遍历出来,不管是私有的属性方法还是有继承的属性方法。但是使用hasOwnProperty之后,只显示私有属性和方法,不显示通过原型链继承的属性和方法。

ES6 的类#

ES6 引入了类(class)的语法,但是它的本质上还是原型。

代码#

class Animal {
  constructor(name) {
    this.name = name;
  }
 
  getName() {
    return this.name;
  }
}
 
class Dog extends Animal {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
 
  speak() {
    return "woof";
  }
}
 
const dog = new Dog("wang", 3);
console.log(
  dog.age + " year old dog " + dog.getName() + " says: " + dog.speak()
); // 3 year old dog wang says: woof

分析#

JavaScript 语言没有像其他语言一样使用类和对象的概念,而是使用了原型克隆,ES6 的类的本质上还是原型,只不过换了一种写法而已,相当于原型的语法糖。

类里面的constructor相当于构造函数的constructor,然后继承使用了extends关键字,创建实例还是使用newthis指向的就是new出来的实例。其实类本质上就是一个构造函数,可以往它的prototype上添加属性和方法,然后new出来的实例还是会继承类的原型上的方法的,这些都是和构造函数完全一样的。

总结#

原型的几个重要特性

  • 任何函数都有一个prototype属性,它指向一个对象,就是原型对象,也是一个普通对象。
  • 任何通过构造函数生成的实例对象都有一个__proto__属性,它也是一个普通对象。
  • 任何对象的__proto__属性指向它的构造函数的prototype属性。
  • 函数的prototype不属于自身的原型链,它是创建子类的核心,决定了子类的数据类型。
  • 而对象的__proto__记录着自己的原型链,决定了自身的数据类型,改变__proto__就等于改变对象的数据类型。
  • 原型链的形成真正是靠__proto__而非prototype
  • 子类的__proto__和父类的__proto__记录的原型链是两条不一样的原型链。