文章目录
构造函数
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的原型对象,简称原型。
现在我们给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对象。
Person构造函数有一个隐式属性指向了他的原型对象Person.prototype,而p1对象也有一个隐式原型__proto__指向了原型对象Person.prototype,而在原型上面我们定义了showName方法。
3. 实例访问属性或者方法的时候,会遵循的原则:
- 如果实例上面存在,就用实例本身的属性和方法。
- 如果实例上面不存在,就会顺着__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实例对象上的方法'
结合代码合图片可以看到,原型上有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
- 每个函数的原型对象上面都有一个constructor属性,指向构造函数本身。
console.log(Person.prototype.constructor === Person); // true
这里我们可以看出Person的原型Person.prototype有一个属性constructor又指向Person构造函数本身。
原型链
在上面我们提到,对象在寻找某一属性,如果自身属性没有找到就去他对应的原型对象去找。若在原型上面找到了对应的属性就停止,否则继续去原型的原型找对应的属性,这样就成了一条原型链。
console.log(Person.prototype.__proto__ === Object.prototype); // true
这个时候来了Object对象,它是JavaScript的顶级对象,同样也是有自己的原型Object.prototype,这时候Person对象以及它的原型,Object对象对应原型关系,如图:
将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