0
点赞
收藏
分享

微信扫一扫

JS原型(prototype),我想给你讲明白

JS的原型可谓是老生常谈的一个问题,网上的资料也是一搜一大把。为什么还想写一篇文章呢?主要还是因为网上大多数文章要么一上来就是一张原型链的图,直接劝退;或者简单的罗列知识点;亦或是站在一个很高的高度上去解释原型,把它说的非常玄乎。
一般人学习新知识都是由难到易、由浅入深的,我想用这种感觉和大家聊聊JS的原型。也算是对自己学习的总结。

1. 什么是原型,为啥要有原型

什么是原型,为啥要有原型呢?这问题一上来确实不好回答,就算给出一个生硬的定义也毫无意义。不妨换个思路思考:如果JS中没有原型会怎么样?

首先,如果没有原型,对象的一些方法我们就无法调用了,比如:

let num = 99.9999;
num.toFixed(2);  // 100.00
num.hasOwnProperty('a'); // false

上面的代码我们定义了一个数值变量,并调用了它toFixed和hasOwnProperty。我们每天都在写这样的代码,但是我们可曾想过,为什么变量会有这些方法,它们是在哪里定义的?

我们能调用这两个方法,借助的就是原型的力量。我们能调用toFixed方法是因为num是Number类型的,更准的说num是基本类型number,这里发生了自动装箱(自动装箱可以看我的另一篇文章,这里就不细说了)。而Number的原型上就定义了toFixed等方法。
你可使用Object.getOwnPropertyNames发现它们:

Object.getOwnPropertyNames(Number.prototype);
(7) ['constructor', 'toExponential', 'toFixed', 'toPrecision', 'toString', 'valueOf', 'toLocaleString']

类的实例可以调用类的方法,这无可厚非。可是Number并没有定义hasOwnProperty方法,为啥能调用呢?这是因为Number是Object的子类,而Object的原型上定义了此方法:

Object.getOwnPropertyNames(Object.prototype)
(12) ['constructor', '__defineGetter__', '__defineSetter__', 'hasOwnProperty', '__lookupGetter__', '__lookupSetter__', 'isPrototypeOf', 'propertyIsEnumerable', 'toString', 'valueOf', '__proto__', 'toLocaleString']

子类的对象可以调用父类的方法,这也无可厚非。

其次,如果没有原型,以上列出的这些方法可能都要以全局方法的方式存在,就像parseIntparseFloat一样。记忆这些方法是件难事,而现在原型将同一类型常用的方法都汇聚到一起,使我们可以直接访问它们。

如果你有面向对象的基础知识的话,你会发现上面我们说的两点,其实就是面向对象三大特性的“封装”和“继承”。

所以什么是原型,为啥要有原型?简单说就是为了面向对象。条条大路通罗马,有些编程语言的面向对象是基于类(class)的,向Java,有些是基于原型(prototype)的,像JS。

上面我故意避开原型以及原型链的一些知识(后面会讲),就是为了方便大家理解。

2. prototype、_proto_、[[Prototype]]

上面介绍为啥要有原型,本节我们来更进一步认识它们。

上面三个词汇你肯定都见过,首先说[[Prototype]],它一般出现在一些书籍和标准规范里,用来表示原型。而真正的实现是prototype__proto__

  • prototype属性只有函数才有;
  • __proto__属性所有对象都有(Object.create(null)除外);

如果直说上面两点,未免有留于定义之嫌,我们还是展开说说。

首先,为什么只有函数才有prototype属性?这个很好解释,因为只有函数才能作为构造方法创建对象。而原型的目的就是面向对象,所以函数就是类的感念,类需要借助prototype来描述它有哪些属性方法。

其次,为什么所有对象都有__proto__属性呢?因为只要是对象就有对应的构造器(Object.create(null)除外),它不可能凭空而来,而__proto__就是用来指向构造器的原型。

下面我们通过代码演示prototype__proto__的关系:

let arr = []; // 定义一个数组
arr.constructor === Array; // true,说明arr的构造器是Array
arr.__proto__ === Array.prototype; // true,__proto__只是构造器原型的引用
arr.splice === Array.prototype.splice; // true,splice方法是在Array.prototype定义的

prototype最常见的用途就是扩展类的方法,尤其是扩展JS内部的类,下面的代码演示了如何给Array添加一个去重的方法:

Array.prototype.distinct = function() {
  return new Array(...new Set(this));
}
let arr = [1, 2, 2, 3, 3, 3];
arr.distinct(); // [1, 2, 3]

相比于prototype__proto__的出场率就没那么高了。尤其是ES6推出了Object.getPrototypeOfObject.setPrototypeOf来代替__proto__。究其原因,我认为主要是__proto__的作用是用来“继承”,而在ES6之前,开发者还是很少使用原型来编写面向对象的程序,尤其是“继承”。

3. 原型链

如果你理解了前面的内容,原型链就很容易了。前面说了对象都有__proto__,而在JS中万物皆对象obj.__proto__也是个对象,它也有__proto__属性,也就是obj.__proto__.__proto__,只要__proto__存在,就会一直寻找下去。这样就形成了一个链条,叫做原型链。
什么时候这个链条会结束呢?通常到Object.prototype结束。

let arr = [];
arr.__proto__; // 等于Array.prototype
arr.__proto__.__proto__; // 等于Object.prototype
arr.__proto__.__proto__.__proto__; // 等于null,结束

这里你可能会问,不是所有对象都有__proto__吗?,为啥Object.prototype.__proto__为null呢?因为它必须为null,否则原型链可能无法结束,就无限递归了。

那可以认为的设置Object.prototype.__proto__吗?不能,JS当然想到了你会这么做,当你尝试给它赋值时,你会得到一个报错:

Object.prototype.__proto__ = Object.prototype
// Uncaught TypeError: Immutable prototype object '#<Object>' cannot have their prototype set

这样以来,除了上面我们提到的Object.create(null),我们又知道了一个没原型的对象Object.prototype

那这个原型链有什么用呢,我介绍两个主要用途:

  1. 继承,当访问对象的属性或方法时,首先从对象自身查找,没有就沿着原型链向上找。
  2. instanceof,instanceof的原理就是借助原型链。

instanceof在我的另一篇文章《js typeof、instanceof区别一次给你讲明白》中有详细介绍,感兴趣的可以去看,这里重点介绍__proto__和继承。

4. 使用__proto__实现继承

在讲解之前,有必要谈一下我理解的继承。我认为继承必须发生在父子类之间。在一些文章中,我看过“数组对象从Array.prototyoe上继承方法”的说法,我认为这是不对的,因为数组对象就是Array的实例,这里只有一个Array类,谈不上继承。
下面我们用DogAnimal来演示继承:

function Animal() { };  // 定义一个Animal类
Animal.prototype.run = function () { console.log('奔跑...'); } // 定义父类方法

function Dog() {} // 定义一个Dog类

// 让Dog继承Animal
Dog.prototype.__proto__ = Animal.prototype; // 利用原型链实现继承
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...

看知乎上有人说下面的代码也是继承的一种方式:

function Animal() { // 定义一个Animal类
	this.run = function () { console.log('奔跑...'); } // 定义实例方法
};

function Dog() { // 定义一个Dog类
	Animal.call(this); // 使用Animal构造函数初始化Dog实例
} 

let dog = new Dog(); // 定义Dog对象
// 调用Animal方法
dog.run(); // 奔跑...

我并不承认上面的代码是继承,首先是它无法“继承”父类原型上定义的属性和方法;其次,也是最重要的,dog instanceof Animal返回false,这也能称之为继承?这只是一种代码复用的方式罢了,可以称之为“混入”。

更可笑的是下面代码:

function Animal() { // 定义一个Animal类
	this.run = function () { console.log('奔跑...'); } // 定义实例方法
};

function Dog() { // 定义一个Dog类
	return new Animal(); // 构造方法返回Animal实例
} 

let dog = new Dog(); // 定义Dog实例

// 调用Animal方法
dog.run(); // 奔跑...
console.log(dog instanceof Dog); // false
console.log(dog instanceof Animal); // true

原谅我笑了,好家伙,直接儿子变父亲!

所以我认为真正的继承只有一种,那就是利用原型链的方式。但是利用原型链的继承也有一个问题:无法继承实例属性和方法,也就是在父类构造函数的定义的属性和方法。比如:

function Animal() {
	this.eat = function() { console.log('疯狂进食...'); } // 实例方法
};  // 定义一个Animal类
Animal.prototype.run = function () { console.log('奔跑...'); } // 原型上定义的方法

function Dog() { } // 定义一个Dog类

// 让Dog继承Animal
Dog.prototype.__proto__ = Animal.prototype; // 利用原型链实现继承
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
dog.eat(); // Uncaught TypeError: dog.eat is not a function

怎么解决呢?其实上面的例子已经有了解决执法,那就是混入:

function Animal() {
	this.eat = function() { console.log('疯狂进食...'); } // 实例方法
};  // 定义一个Animal类
Animal.prototype.run = function () { console.log('奔跑...'); } // 原型上定义的方法

function Dog() { // 定义一个Dog类
	Animal.call(this); // 这里就复用了eat方法
} 

// 让Dog继承Animal
Dog.prototype.__proto__ = Animal.prototype; // 利用原型链实现继承
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
dog.eat(); // 疯狂进食...

上面的继承其实已经很棒了,但还有优化的空间。因为父类的属性和方法需要顺着原型链来查找,如果原型链很长,那么就会有一定的性能损失。我们可以使用混入的方式,我们可以借助Object.create方法将父类原型上定义的方法,复制到子类原型上:

function Animal() {
	this.eat = function() { console.log('疯狂进食...'); } // 实例方法
};  // 定义一个Animal类
Animal.prototype.run = function () { console.log('奔跑...'); } // 原型上定义的方法

function Dog() { // 定义一个Dog类
	Animal.call(this); // 混入,解决实例属性的继承问题
} 

// 让Dog继承Animal
Dog.prototype = Object.create(Animal.prototype); // 利用父类的原型创建一个实例,目的是复制父类原型的属性和方法,到子类原型上
Dog.prototype.constructor = Dog;  // 因为Dog.prototype是Animal的实例,所以修复前Dog.prototype.constructor为Animal(详细可以去了解constructor的含义)。
// 在子类原型上第一方法时,需要在Object.create之后
Dog.prototype.woof = function() { console.log('汪汪汪...') };

let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
dog.eat(); // 疯狂进食...
dog.woof(); // 汪汪汪...

现在调用父类的方法,就可以直接在自己的原型上找到了,而不用顺着原型链一致网上找。
还可以再优化吗?还可以。这里我们没有将静态属性和方法,静态属性和方法就行直接定义在类上的方法,也就是直接定义在函数上的方法。

function Animal() {
	this.eat = function() { console.log('疯狂进食...'); } // 实例方法
};  // 定义一个Animal类

Animal.KIND = '哺乳动物'; // 静态属性
Animal.printKind = function() { console.log(Animal.KIND); } // 静态方法

能不能让子类继承父类的静态成员?虽然这很苛刻,毕竟Java继承也是不包括静态成员的,但仍然能够实现:

function Animal() {
	this.eat = function () { console.log('疯狂进食...'); } // 实例方法
};  // 定义一个Animal类
Animal.KIND = '哺乳动物'; // 静态属性
Animal.printKind = function () { console.log(Animal.KIND); } // 静态方法
Animal.prototype.run = function () { console.log('奔跑...'); } // 原型上定义的方法

function Dog() { // 定义一个Dog类
	Animal.call(this); // 混入,解决实例属性的继承问题
}

// 让Dog继承Animal
Dog.prototype = Object.create(Animal.prototype); // 利用父类的原型创建一个实例,目的是复制父类原型的属性和方法,到子类原型上
Dog.prototype.constructor = Dog;  // 因为Dog.prototype是Animal的实例,所以修复前Dog.prototype.constructor为Animal(详细可以去了解constructor的含义)。

Dog.__proto__ = Animal; // 这里是把函数当成对象看待

// 在子类原型上第一方法时,需要在Object.create之后
Dog.prototype.woof = function () { console.log('汪汪汪...') };

let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
dog.eat(); // 疯狂进食...
dog.woof(); // 汪汪汪...
Dog.KIND;    // 哺乳动物
Dog.printKind(); // 哺乳动物

上面的写法差不多是不借助ES6语法情况下,写出的比较完美的继承了。

5. ES6的面向对象

但就原型的知识,上面的内容已经足够了。本节是一些扩展,不感兴趣,可以不用了解。
我们都知道ES6引入了基于class的面向对象,你可以用更标准的方式实现面向对象。前面的例子,我们可以用class实现:

class Animal { // 父类
	constructor() {
		this.eat = function() { console.log('疯狂进食...'); } // 实例方法
	}
	run() { console.log('奔跑...'); }
}
class Dog extends Animal { // 子类继承父类
	woof() { console.log('汪汪汪...') }
}
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
// 调用父类的实例方法
dog.eat(); // 疯狂进食...
dog.woof(); // 汪汪汪...

当然本节的内容不是介绍ES6的class,而是告诉你,ES6的class只是语法糖,其本质还是原型。上面的代码可以使用Babel将其编译成ES6之前的写法:

"use strict";

function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }

function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var Animal = /*#__PURE__*/function () {
  // 父类
  function Animal() {
    this.eat = function () {
      console.log('疯狂进食...');
    }; // 实例方法

  }

  var _proto = Animal.prototype;

  _proto.run = function run() {
    console.log('奔跑...');
  };

  return Animal;
}();

  // 静态属性
_defineProperty(Animal, "KIND", '哺乳动物');
  // 静态方法
_defineProperty(Animal, "printKind", function () {
  console.log(Animal.KIND);
});

var Dog = /*#__PURE__*/function (_Animal) {
  _inheritsLoose(Dog, _Animal);

  function Dog() {
    return _Animal.apply(this, arguments) || this;
  }

  var _proto2 = Dog.prototype;

  // 子类继承父类
  _proto2.woof = function woof() {
    console.log('汪汪汪...');
  };

  return Dog;
}(Animal);

代码虽多,但仔细看就会发现,使用的技巧都是前文讲过的,如果有不懂的,再反复看看第4小结的内容。

6. 结语

最后,码字不已,还望支持。

举报

相关推荐

0 条评论