什么是原型
在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象。
- 一种是基于类来描述对象。
- 在这类语言中,总是先有类,再从类去实例化一个对象。类与类之间又可能会形成继承、组合等关系。
- 基于原型的面向对象系统通过“复制”的方式来创建新对象。一些语言的实现中,还允许复制一个空对象。这实际上就是创建一个全新的对象。
- 另一种是基于原型来描述对象。
- 这种更倾向于关注一系列对象实例的行为,再关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是将它们分成类。
- JavaScript的原型并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用。
JavaScript的原型
- 如果所有对象都有私有字段
[[prototype]],就是对象的原型; - 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。
es6新增的访问操纵原型方法
- Object.create 根据指定的原型创建新对象,原型可以是null;
- Object.getPrototypeOf 获得一个对象的原型;
- Object.setPrototypeOf 设置一个对象的原型。
浏览器中也可以通过对象的
__proto__属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。但是,__proto__属性本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性。
早期版本的类与原型
在早期版本的JavaScript中,“类”的定义是一个私有属性 [[class]],可以通过Object.prototype.toString来访问,可以用这个方法来判断变量的类型。
Object.prototype.toString.call('123'); // "[object String]"
Object.prototype.toString.call(123); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
在ES5开始,[[class]] 私有属性被Symbol.toStringTag代替,我们可以自定义Object.prototype.toString的行为。
var o = { [Symbol.toStringTag]: "MyObject" }
console.log(o + ""); // [object MyObject]
构造函数
JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓“构造函数”,就是专门用来生成实例对象的函数。
var Vehicle = function () {
this.price = 1000;
};
上面就是一个构造函数,其实就是一个普通的函数,但是有自己的特征:
- 函数体内部使用了
this关键字,代表了所要生成的对象实例。 - 生成对象的时候,必须使用
new命令。 - 为了与普通函数区别,构造函数名字的第一个字母通常大写。
new 命令
基本用法
new命令的作用,就是执行构造函数,返回一个实例对象。
var Vehicle = function () {
this.price = 1000;
};
var v = new Vehicle();
v.price // 1000
在这个过程中,实际做了几件事(具体步骤见下文):
- 创建新的实例对象;
new命令执行时,构造函数内部的this,就代表了新生成的实例对象,this.price表示实例对象有一个price属性,值是1000。- 返回新建的对象。
注意:new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。
// 推荐的写法
var v = new Vehicle();
// 不推荐的写法
var v = new Vehicle;
上面的两种写法是等价的,但为了表示调用函数,推荐添加上括号。
判断是否通过new调用
如果忘了使用new命令,那么构造函数就作为一个普通函数执行,无法返回我们需要的实例对象。
var Vehicle = function (){
this.price = 1000;
};
var v = Vehicle(); // 这时Vehicle里的this,代表全局对象
v // undefined
price // 1000
为了保证构造函数必须与new命令一起使用,有以下几个方法:
- 构造函数内部使用严格模式
function Vehicle(foo, bar){
'use strict';
this.price = 1000;
}
Vehicle()
// TypeError: Cannot set property 'price' of undefined
在严格模式中,this不代表全局变量,表示为undefined,所以这里直接执行构造函数会报错。
- 构造函数内部判断是否使用
new命令
function Vehicle() {
if (!(this instanceof Vehicle)) {
return new Vehicle();
}
this.price = 1000;
}
Vehicle().price // 1000
(new Vehicle()).price // 1000
instanceof运算符,用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上。
- 使用
new.target
new.target属性允许你检测函数或构造方法是否是通过new运算符被调用的。在通过new运算符被初始化的函数或构造方法中,new.target返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target的值是undefined。
所以,上面的方法我们可以这样修改下:
function Vehicle() {
if (!new.target) {
return new Vehicle();
}
this.price = 1000;
}
Vehicle().price // 1000
(new Vehicle()).price // 1000
new 命令的原理
上面我们了解了new命令会生成一个实例对象,那么这个实例对象是如何产生的呢?
当我们使用new操作符调用函数时,JavaScript 为我们做了这些事:
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的
prototype属性(注意与对象的私有字段[[prototype]]的区分)。 - 将这个空对象赋值给函数内部的
this关键字。 - 开始执行构造函数内部的代码。
- 返回对象。
注意:如果构造函数内部有return语句,而且**return后面跟着一个对象,new命令会返回return语句指定的对象**;否则,就会不管return语句,返回this对象。
// 返回this对象
function Vehicle(){
this.price = 1000;
return 2000;
}
new Vehicle().price; // 1000
// 返回return对象
function Vehicle1(){
this.price = 1000;
return { price: 2000};
}
new Vehicle1().price; // 2000
若是构建函数中没有对this对象的操作,也没有return后跟着一个对象,则返回一个空对象。
function Vehicle(){
return 2000;
}
new Vehicle(); // {}
所以,我们可以用下面的代码来简化下new的执行逻辑。
function Vehicle(price){
this.price = price;
}
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ ...params) {
// 1.创建一个空对象
const createObj = {};
// 2.将空对象的原型,指向构造函数的`prototype`属性
Object.setPrototypeOf(createObj, constructor.prototype);
// 3.将这个空对象赋值给函数内部的`this`关键字,并执行构造函数
const resObj = constructor.call(createObj, ...params);
// 4.返回创建的对象
return (typeof resObj === 'object' && resObj != null) ? result : createObj;
}
// 实例
const actor = _new(Vehicle, 1000); // {price: 1000}
对象继承方式
原型链继承
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child() {
this.age = 18;
}
Child.prototype = new Parent();
const child = new Child();
child.sayName();
优点:
- 操作简单:只需将子类的原型设置为父类的实例即可
缺点:
- 共享原型属性:多个实例可共享父类的对象、数组等数据
- 无法传参:创建子类时无法通过子类传递参数给父类构造函数
构造函数继承
在子类的构造函数中,使用call 或 apply 方法来调用父类的构造函数。
function Parent(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
function Child(name) {
Parent.call(this, name);
this.age = 18;
}
const child = new Child('Alice');
console.log(child.name);
console.log(child.age);
优点:
- 属性独立:子类型可以拥有父类型实例的独立属性
- 能传参:多个实例能给父类传参
缺点:
- 不继承原型属性和方法:只继承父类型的实例属性,不继承父类型的原型属性和方法
- 性能问题:方法都在构造函数中定义,每次创建实例都会创建一遍方法,会有性能损耗
组合继承
function Parent(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent ();
Child.prototype.constructor = Child ;
const child = new Child('Alice', 18);
child.sayName();
console.log(child.colors);
虽然不设置 Child.prototype.constructor = Child 不一定会影响代码的基本功能,但为了保持代码的清晰性、遵循编程惯例以及避免潜在的问题,建议设置下。
优点:
- 属性独立
- 能传参
- 继承原型属性和方法
缺点:
- 性能问题:调用了两次父类的构造函数,且子类有冗余属性
寄生组合式继承
function inheritPrototype(child, parent) {
// 创建对象,创建父类原型的一个副本
let prototype = Object.create(parent.prototype);
// 增强对象,弥补因重写原型而失去的默认的constructor属性
prototype. constructor = child;
// 指定对象,将新创建的对象赋值给子类的原型
child. prototype = prototype;
}
function Parent(name) {
this.name = name;
this.num = [0, 1, 2];
}
Parent.prototype.sayName = function () {
alert(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
const child = new Child('Alice', 20);
child.sayName(); // 输出 'Alice'
console.log(child.num); // 输出 [0, 1, 2]
- 使用Object.create(parent.prototype),减少一次父类对象的实例调用
- 修改子类对象的prototype,基础父类对象的属性,减少一次父类对象的实例调用
ES6 中的类
在ES6中加入了新特性class,我们可以通过ES6的语法来定义类,而令function回归原本的函数语义。
class NewVehicle {
constructor(price) {
this.price = price;
}
}
new NewVehicle().price; // 1000
此外,类提供了继承能力。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
class Dog extends Animal {
constructor(name) {
super(name); // call the super class constructor and pass in the name parameter
}
speak() {
console.log(this.name + ' barks.');
}
}
let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.