原型和原型链

构造函数

JavaScript通过构造函数生成新对象,所以构造函数可以看做是对象的模板。实例对象的属性和方法可以定义在构造函数内部。

function Cat(name, color){
    this.name = name;
    this.color = color;
}

let cat1 = new Cat('叮当', '白色');

console.log(cat1.name) // 叮当
console.log(cat1.color) // 白色

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

同一个构造函数的多个实例之间,无法共享实例,从而造成对系统资源的浪费。

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

let cat1 = new Cat('叮当1', '白色');
let cat2 = new Cat('叮当2', '灰色');

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

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

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

prototype-原型对象

在对象实例和和它的构造器之间建立一个连接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过原型链,在构造器中找到这些属性和方法。

1. 每个函数上面都有一个属性(prototype)执行了函数的原型对象(Person.prototype)。

function Person(){
    // ....
}
console.log(Person.prototype);

即使只定义了一个空函数,也存在一个prototype的属性。

{constructor: ƒ}
constructor: ƒ Person()
arguments: null
caller: null
length: 0
name: "Person"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: 数组排序.html:6
[[Scopes]]: Scopes[1]
__proto__:
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()

// 这只是一部分,可以自己打印看下具体的内容。

尽管我们只是创建了空对象,但是浏览器在内存中创建了两个对象:Person(函数)和Person.prototype,其中,我们称Person为构造函数,因为我们要用这个函数来new对象,Person.prototype称为Person的原型对象,简称原型。

image

现在我们给Person构造函数添加并使用new方式创建一个Person()对象

function Person(name,age){
    this.name = name;
    this.age = age;
}

Person.prototype.showName = function(){
    return this.name;
}

let p1 = new Person('lisa', 22);
console.log(p1.showName()); //lisa

2. 每个实例上面都有一个隐式原型( proto)指向了函数的原型对象。

如上例子p1对象有一个隐式原型也指向了Person.prototype对象。

image

Person构造函数有一个隐式属性指向了他的原型对象Person.prototype,而p1对象也有一个隐式原型__proto__指向了原型对象Person.prototype,而在原型上面我们定义了showName方法。

3. 实例访问属性或者方法的时候,会遵循的原则:

  1. 如果实例上面存在,就用实例本身的属性和方法。
  2. 如果实例上面不存在,就会顺着__proto__的指向一直往上查找,找到就停止。
function Person(name,age){
    this.name = name;
    this.age = age;
}

Person.prototype.showName = function(){
    return ''调用的原型对象上的方法'';
}

let p1 = new Person('lisa', 22);
p1.showName = function(){
    return '调用的是p1实例对象上的方法'
}
console.log(p1.showName()) // 调用的是p1实例对象上的方法

let p2 = new Person('lisa', 22);
console.log(p2.showName()) // '调用的是p1实例对象上的方法'

image

结合代码合图片可以看到,原型上有showName方法,p1对象也有showName方法,这个时候p1调用的实例自身的showName方法,而p2实例对象自身没有showName方法,这个时候会顺着p2对象的__proto__属性指向的原型查找也没有showName方法,如果没有找到对应的方法,就会顺着原型的原型去找对应的方法,最终找到Object对象,如果还没有找到则报undefined。

console.log(p1.showName === p2.showName); // false
console.log(p2.sex) // undefined
// 因为p2的实例没有sex方法,找到原型上也没有找到,知道最后返回undefined
  1. 每个函数的原型对象上面都有一个constructor属性,指向构造函数本身。
console.log(Person.prototype.constructor === Person); // true

image

这里我们可以看出Person的原型Person.prototype有一个属性constructor又指向Person构造函数本身。

原型链

在上面我们提到,对象在寻找某一属性,如果自身属性没有找到就去他对应的原型对象去找。若在原型上面找到了对应的属性就停止,否则继续去原型的原型找对应的属性,这样就成了一条原型链。

console.log(Person.prototype.__proto__ === Object.prototype); // true

这个时候来了Object对象,它是JavaScript的顶级对象,同样也是有自己的原型Object.prototype,这时候Person对象以及它的原型,Object对象对应原型关系,如图:

image

将Object和Person联系起来的关键是Person.prototype的属性__proto__,它指向了Object.prototye,它将两者打通,构成了一个链式关系,同时Object的prototype也指向了Object.prototype,所以

console.log(Person.prototype.__proto__ === Objcet.prototype) // true
function Person(name,age){
    this.name = name;
    this.age = age;
}

Person.prototype.showName = function(){
    return '调用的是原型对象上的方法';
}

let p1 = new Person('lisa', 22);
let p2 = new Person('Tony', 25);

p2.__proto__ = null;

console.log(p1.showName());
console.log(p2.showName());

现在我们打破了p2对象的原型,它原本指向的是Person的原型,现在指向了null,所以控制台会报错:

Uncaught TypeError: p2.showName is not a function

现在继续构造一个对象Animal,然后强制修改p2的原型链,让他指向Animal的原型。

function Person(name,age){
    this.name = name;
    this.age = age;
}

Person.prototype.showName = function(){
    return '调用的是原型对象上的方法';
}

// 定义另一个构造函数
function Animal(){
    
}

// 在Animal的原型上面定义方法

Animal.prototype.showName = function(){
    return '我是Animal的showName';
}

let p1 = new Person('lisa', 22);
let p2 = new Person('Tony', 25);

p2.__proto__ = Animal.prototype; // 将p2的__proto__指向Animal的原型

console.log(p1.showName()) // 调用的是原型对象上的方法
console.log(p2.showName()) // 我是Animal的showName

从代码来看,p2的showName方法调用的Animal原型上面的showName方法,而不是Person原型上的showName,我们来验证一下:

console.log(p2.showName() === Animal.prototype.showName() ) // true

一般来说,不建议手动去修改某个对象的原型,这样会破坏原来的原型链。

原型链的作用:读取对象的某个属性时,JavaScript先寻找对象本身的属性,如果找不到就到它的原型去找,如果还是找不到,就到原型的原型找。直到最顶层的Object.prototype还是找不到,则返回undefined

如果对象自身和它的原型都定义了一个同名属性,那么优先读取对象自身的属性,这叫“覆盖”。

一级一级向上,在原型链寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

如果让某个函数的prototype属性指向一个数组,就意味着改函数可以当做数组的构造函数,因为它生成的实例对象都可以通过prototype属性调用数组的方法。

var MyArray = function () {};

MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;

var mine = new MyArray();
mine.push(1, 2, 3);

mine.length // 3
mine instanceof Array // true

代码中,mine是构造函数MyArray的实例对象,由于MyArray的prototype属性指向一个数组实例,使得mine可以调用数组方法(这些方法定义在数组实例的prototype对象上面)。至于最后的instanceof表达式,我们知道instanceof运算符用来比较一个对象是否为某个构造函数的实例,代码中则表示mine是Array的实例。

下面代码,某个属性到底是原型链上哪个对象自身的属性。

function getDefiningObject(obj, propKey) {
  while (obj && !{}.hasOwnProperty.call(obj, propKey)) {
    obj = Object.getPrototypeOf(obj);
  }
  return obj;
}

prototype属性的作用

JavaScript的每个对象都会继承另一个对象,后者称为"原型"(prototype)对象。

任何一个对象,都可以充当其他对象的原型;
由于原型对象也是对象,所以它也有自己的原型;
null也可以充当原型,区别在于它没有自己的原型对象;

JavaScript继承机制的设计就是,原型的所有属性和方法,都能被子对象共享。

每一个构造函数都有一个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'

代码中,构造函数Animal的prototype对象,就是实例对象cat1和cat2的原型对象。原型对象上添加一个color属性,结果,实例对象都继承了该属性。

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

代码中,原型对象的color属性值变为yellow,两个实例对象的color属性立刻变了,这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到构造函数的prototype属性指向的对象去寻找该属性和方法。

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

cat1.color = 'black';

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

代码中,实例对象cat1的color属性改为black,这个时候就不会再去原型对象读取color属性,而cat2依然是yellow。

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

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

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

构造函数就是普通的函数,所以实际上所有函数都有prototype属性。

constructor属性

prototype对象有一个constructor属性,默认指向prototype所在的构造函数。

function P() {}

P.prototype.constructor === P
// true

由于constructor属性定义在prototype对象上,意味着可以被所有实例对象继承。

function P() {}
var p = new P();

p.constructor
// function P() {}

p.constructor === P.prototype.constructor
// true

p.hasOwnProperty('constructor')
// false

代码中,p是构造函数P的实例对象,但是p自身免疫constructor属性,改属性其实是读取原型链上面的P.prototype.constructor属性。

constructor属性的作用:分辨原型对象到底属于哪个构造函数

function F() {};
var f = new F();

f.constructor === F // true
f.constructor === RegExp // false

代码中,使用constructor属性,确定实例对象f的构造函数是F,而不是RegExp。

有了constructor属性,就可以从实例新建另一个实例。

function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true

代码中,x是构造函数Constr的实例,可以从x.constructor间接调用构造函数。

这使得在实例方法中,调用自身的构造函数成为可能。

Constr.prototype.createCopy = function () {
  return new this.constructor();
};

constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错。

function Person(name) {
  this.name = name;
}

Person.prototype.constructor === Person // true

Person.prototype.copy = function () {
  return new this.constructor(this.name);
};

代码中,Person构造函数的原型对象constructor属性指向Person。然后,在原型上定义了copy方法,改方法内部通过this.constructor调用Person。如果原型对象变了,这个constructor属性的指向可能就会出错。

Person.prototype = {
  method: function () {}
};

var p1 = new Person('张三');
p1.copy() // TypeError: p1.copy is not a function

代码中,Person.prototye改成另一个对象,但是没有改写constructor属性,结果导致调用实例方法报错。

所以,修改原型对象时,一般要同时校正constructor属性的指向。

/ 避免这种写法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

// 较好的写法
C.prototype = {
  constructor: C,
  method1: function (...) { ... },
  // ...
};

// 更好好的写法
C.prototype.method1 = function (...) { ... };

代码中,避免完全覆盖掉原来的prototype属性,要么想constructor属性重新指向原来的构造函数,要么只在原型对象上添加方法,这样可以保证instanceof运算符不会失真。

通过name属性,可以从实例得到构造函数的名称。

function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"

instanceof运算符

instanceof运算符返回一个布尔值,表示某个对象是否为指定的构造函数的实例

var v = new Vehicle();
v instanceof Vehicle // true

代码中,对象v是构造函数Vehicle的实例,所以返回true。

instanceof运算符的左边是实例对象,右边是构造函数。它会检查右边构造函数的原型对象(prototype),是否在左边对象的原型链上。

v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)

由于instanceof对整个原型链上的对象都有效,因此同一个实例对象,可能会对多个构造函数都返回true。

var d = new Date();
d instanceof Date // true
d instanceof Object // true

代码中,d同时是Date和Object的实例,因此对这两个构造函数都返回true。

instanceof的原理就是检查原型链,对于那些不存在原型链的UI想,无法判断。

Object.create(null) instanceof Object // false

代码中,Object.creat(null)返回的新对象的原型是你又来了,即不存在原型,因此instanceof就认为对象不是Object实例。

instanceof运算符只能用于对象,不适用于原始类型的值。

var s = 'hello';
s instanceof String // false

代码中,字符串不是String对象的实例(因为字符串不是对象),所以返回false。

此外,对于undefined和null,instanceof运算符总是返回false。

undefined instanceof Object // false
null instanceof Object // false

利用instanceof运算符,还可以巧妙地解决调用构造函数时忘了加new命令的问题。

function Fubar (foo, bar) {
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else {
    return new Fubar(foo, bar);
  }
}

代码中使用instanceof运算符,在函数体内部判断this关键词是否为构造函数Fubar的实例。如果不是,就表明忘了加new命令。

Object.getPrototypeOf()

Object.getPrototypeOf()方法返回一个对象的原型。这个是获取原型对象的标准方法。

// 空对象的原型是Object.prototype
Object.getPrototypeOf({}) === Object.prototype
// true

// 函数的原型是Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype
// true

// f 为 F 的实例对象,则 f 的原型是 F.prototype
var f = new F();
Object.getPrototypeOf(f) === F.prototype
// true

Object.setPrototypeOf()

Object.setPrototypeOf()方法可以为现有对象设置原型,返回一个新对象。

Object.setPrototypeOf()方法接受两个参数,第一个是现有对象,第二个是原型对象。

var a = {x: 1};
var b = Object.setPrototypeOf({}, a);
// 等同于
// var b = {__proto__: a};

b.x // 1

代码中,b对象是Object.setPrototypeOf()方法返回一个新对象,该对象本身为空,原型为a对象,所以b对象可以拿到a对象的所有属性和方法。b对象本身并没有x属性,但是JavaScript引擎找到它的原型对象a,然后读取a的x属性。

new命令通过构造函数新建实例对象,实质就是将实例对象的原型,指向构造函数的prototype属性,然后在实例对象上执行构造函数。

var F = function () {
  this.foo = 'bar';
};

var f = new F();

// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);

Object.create()

生成实例对象的常用方法,就是用用new命令,让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,他可能根本不是由构造函数生成的。能不能从一个实例对象,生成另一个实例对象呢?

JavaScript提供了Object.create()方法,用来满足这种需求。该方法接受一个对象作为参数,然后已它为原型,返回一个实例对象。改实例完全继承原型对象的属性。

// 原型对象
var A = {
  print: function () {
    console.log('hello');
  }
};

// 实例对象
var B = Object.create(A);
B.print() // hello
B.print === A.print // true

代码中,Object.create()方法一A对象为原型,生成了B对象。B继承了A的所有属性和方法。这段代码等同于下面

var A = function () {};
A.prototype = {
 print: function () {
   console.log('hello');
 }
};

var B = new A();
B.print === A.prototype.print // true

实际上,Object.create()方法可以用下面的代码代替。如果老式浏览器不支持Object.create()方法,可以用这个代码自己部署。

if (typeof Object.create !== 'function') {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

代码表明,Object.create()方法的是指是新建一个构造函数F,然后让F.prototype属性指向参数对象obj,最后返回一个F的实例,从而实现让改实例继承obj的属性。

下面三种方式生成的新对象是等价的

var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();

如果想要生成一个不继承任何属性(比如没有toString和valueOf方法)的对象,可以将Object.create的参数设为null。

var obj = Object.create(null);

obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'

代码中,对象obj的原型是null,它就不具备一些定义在Object.prototype对象上的属性,比如valueOf方法。

使用Object.create方法的时候,必须提供对象原型,即参数不能为空,或者不是对象,否则会报错。

Object.create()
// TypeError: Object prototype may only be an Object or null

Object.create(123)
// TypeError: Object prototype may only be an Object or null

Object.create方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上。

var obj1 = { p: 1 };
var obj2 = Object.create(obj1);

obj1.p = 2;
obj2.p
// 2

代码中,修改对象原型obj1会影响到新生成的实例对象obj2。

除了对象的原型,Object.create方法还可以接受第二个参数,该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。

var obj = Object.create({}, {
  p1: {
    value: 123,
    enumerable: true,
    configurable: true,
    writable: true,
  },
  p2: {
    value: 'abc',
    enumerable: true,
    configurable: true,
    writable: true,
  }
});

// 等同于
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';

Object.create方法生成的对象,继承了他的原型对象的构造函数。

function A() {}
var a = new A();
var b = Object.create(a);

b.constructor === A // true
b instanceof A // true

代码中,b对象的原型是a对象,因此继承了a对象的构造函数A。

Object.prototype.isPrototypeOf()

对象实例isPrototypeOf方法,用来判断一个对象是否是另一个对象的原型。

var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);

o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true

代码表明,只要某个对象处在原型链上,isPrototypeOf都返回true。

Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false

代码中,有雨Object.prototype处于原型链的最终端,所以对各种实例都返回true,只有继承null的对象除外。

Object.prototype.proto

__proto__属性可以改写成某个对象的原型对象。

var obj = {};
var p = {};

obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true

代码中,通过__proto__属性,将p对象设为obj对象的原型。

根据语言标准,__proto__属性只有浏览器才需要部署,其他环境可以没有这个属性,而且前后的两根下划线,表示它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用Object.getPrototypeof()(读取)和Object.setPrototypeOf()(设置),进行原型对象的读写操作。

原型链可以用__proto__很直观地表示。

var A = {
  name: '张三'
};
var B = {
  name: '李四'
};

var proto = {
  print: function () {
    console.log(this.name);
  }
};

A.__proto__ = proto;
B.__proto__ = proto;

A.print() // 张三
B.print() // 李四

代码中,A对象和B对象的原型都是proto对象,它们都共享proto对象的print方法。也就是说,A和B的print方法,都是在调用proto对象的print方法。

A.print === B.print // true
A.print === proto.print // true
B.print === proto.print // true

可以使用Object.getPrototypeOf方法,检查浏览器是否支持__proto__属性,老式浏览器不支持这个属性。

Object.getPrototypeOf({ __proto__: null }) === null

代码中,将一个对象的__proto__属性设为null,然后使用Object.getPrototypeOf方法获取这个对象的原型,判断是否等于null。如果当前环境支持__proto__属性,两者的比较结果应该是true。

获取原型对象方法的比较

如前所述,__proto__属性指向当前对象的原型对象,即构造函数的prototype属性。

var obj = new Object();

obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true

代码中,首先新建了一个对象obj,它的__proto__属性,指向构造函数(Object或obj.constructor)的prototype属性。所以,两者比较以后,返回true。

因此,获取实例对象obj的原型对象,有三种方法:

  • obj.proto
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

上面三种方法之中,前两种都不是很可靠。最新的ES6标准规定,__proto__属性只有浏览器才需要部署,其他环境可以不部署。而obj.constructor.prototype在手动改变原型对象时,可能会失效。

var P = function () {};
var p = new P();

var C = function () {};
C.prototype = p;
var c = new C();

c.constructor.prototype === p // false

代码中,C构造函数的原型对象被改成了p,结果c.constructor.prototype就失真了。所以,在改变原型对象时,一般要同时设置constructor属性。

C.prototype = p;
C.prototype.constructor = C;

c.constructor.prototype === p // true

所以,推荐使用第三种Object.getPrototypeOf方法,获取原型对象。

var o = new Object();
Object.getPrototypeOf(o) === Object.prototype
// true
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值