本文带大家了解了原型及原型链等相关知识,包括构造函数,继承方式,new的原理,手写new,Object.create方法......
原型
原型是 JavaScript 中的一个重要概念,它是构成 JavaScript 对象模型的基础之一,用于实现对象之间的继承和属性共享。
在 JavaScript 中,每个对象都有一个原型(prototype)。原型是一个对象,它包含可以被其他对象继承的属性和方法。当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法的定义或者到达原型链的顶端。
原型链是由一系列对象连接而成的链条,每个对象都有一个指向它的原型的引用。如果一个对象在自身的属性和方法中没有找到所需的内容,它会继续在原型对象上查找,而原型对象本身也可以拥有自己的原型,这样就形成了原型链。
原型链的使用有助于对象之间的属性和方法的共享,减少了重复定义和内存消耗。通过原型继承,我们可以创建新的对象,并继承已有对象的属性和方法,从而实现代码的重用和扩展。
因此,原型在 JavaScript 中是一个非常有用和强大的概念,并且是面向对象编程的基石之一。它为 JavaScript 提供了一种灵活而强大的继承机制,使得对象可以共享属性和方法,并且可以根据需要进行扩展和修改。原型只是一种编程概念,用于实现对象之间的关系和继承。
js
function Person(name) {
this.name=name
}
console.log(Person.prototype)
1. {constructor: ƒ}
1. 1. constructor: ƒ Person(name)
1. [[Prototype]]: Object
可以看到,原型对象有一个自有属性constructor
,这个属性指向该函数 Person.prototype.constructor==person1.constructor
,同时person1.__proto__.constructor==person1.constructor
(person1是Person的实例),我们平时通过 实例.constructor拿到的构造函数都是通过__proto__向上拿到了的constructor。
constructor
属性的主要作用是:
- 标识对象的构造函数:通过访问对象的
constructor
属性,可以知道该对象是由哪个构造函数创建的。 - 方便对象的类型判断:通过比较对象的
constructor
属性与特定构造函数的引用,可以判断对象的类型是否与该构造函数匹配。
在后文中,会有通过Object.create()方法去创建原型对象并赋值给子类原型对象的操作,此时需要手动挂载consturctor属性 ,或者通过Object.create()方法的第二个参数实现:
javascript
// 子类继承父类
Rectangle.prototype = Object.create(Shape.prototype, {
// 如果不将 Rectangle.prototype.constructor 设置为 Rectangle,
// 它将采用 Shape(父类)的 prototype.constructor。
// 为避免这种情况,我们将 prototype.constructor 设置为 Rectangle(子类)。
constructor: {
value: Rectangle,
enumerable: false,
writable: true,
configurable: true,
},
});
javascript
Person.prototype.sayHello = function() {
console.log("Hello, " + this.name);
};
const person = new Person("John");
console.log(person.constructor); // 输出: [Function: Person]
console.log(person.constructor === Person); // 输出: true
请注意,constructor
属性通常来自构造函数的 prototype
属性。person.constructor返回的是Person函数的引用,而不是简单的函数名字符串。
原型链
JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
备注: 遵循 ECMAScript 标准,符号 someObject.[[Prototype]]
用于标识 someObject
的原型。内部插槽 [[Prototype]]
可以通过 Object.getPrototypeOf()
和 Object.setPrototypeOf()
函数来访问。这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 __proto__
访问器。为在保持简洁的同时避免混淆,在我们的符号中会避免使用 obj.__proto__
,而是使用 obj.[[Prototype]]
作为代替。其对应于 Object.getPrototypeOf(obj)
。 可以看MDN官方给出的示例:
继承属性:
js
const o = {
a: 1,
b: 2,
// __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
__proto__: {
b: 3,
c: 4,
__proto__: {
d: 5,
},
},
};
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null
console.log(o.d); // 5
可以通过手动设置__proto__来改变他向上查找的层级,直到找到或者为null。 通过这个示例可以很好了解原型链向上查找的规则。
继承方法:
js
const parent = {
value: 2,
method() {
return this.value + 1;
},
};
console.log(parent.method()); // 3
// 当调用 parent.method 时,"this"指向了 parent
// child 是一个继承了 parent 的对象
const child = {
__proto__: parent,
};
console.log(child.method()); // 3
// 调用 child.method 时,"this"指向了 child。
// 又因为 child 继承的是 parent 的方法,
// 首先在 child 上寻找"value"属性。但由于 child 本身
// 没有名为"value"的自有属性,该属性会在
// [[Prototype]] 上被找到,即 parent.value。
child.value = 4; // 在 child,将"value"属性赋值为 4。
// 这会遮蔽 parent 上的"value"属性。
// child 对象现在看起来是这样的:
// { value: 4, __proto__: { value: 2, method: [Function] } }
console.log(child.method()); // 5
// 因为 child 现在拥有"value"属性,"this.value"现在表示
// child.value
{ value: 4, __proto__: { value: 2, method: [Function] } }
,通过这种方式可以很好判断this.value到底访问的是哪个value!!当使用方法去获取value时,查看到当前对象中就存在value,就不会向原型链中去寻找value。
构造函数与New
javascript
// 一个构造函数
function Box(value) {
this.value = value;
}
// 使用 Box() 构造函数创建的所有盒子都将具有的属性
Box.prototype.getValue = function () {
return this.value;
};
const boxes = [new Box(1), new Box(2), new Box(3)];
构造函数需要使用new去调用,在"new"的过程中,主要实现四件事:
- 创建一个空的简单 JavaScript 对象(即
{}
); - 为步骤 1 新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象; - 将步骤 1 新创建的对象作为
this
的上下文; - 如果该函数没有返回对象 ,则返回
this
。(正常情况下构造函数是不会有返回值的,如果返回非对象,则返回值不生效;如果返回对象a,则new 构造函数的最终结果就是返回的对象a)
接下来我们来手写new,看看到底如何实现,从代码层面帮助大家理解:
javascript
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'
new
通过构造函数Person
创建出来的实例可以访问到构造函数中的属性new
通过构造函数Person
创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来)
现在在构建函数中显式加上返回值,并且这个返回值是一个原始类型
javascript
function Test(name) {
this.name = name
return 1
}
const t = new Test('xxx')
console.log(t.name) // 'xxx'
可以发现,构造函数中返回一个原始值,然而这个返回值并没有作用
下面在构造函数中返回一个对象
javascript
function Test(name) {
this.name = name
console.log(this) // Test { name: 'xxx' }
return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'
从上面可以发现,构造函数如果返回值为一个对象,那么这个返回值会被正常使用
这样当我们使用new去创建实例的时候,实例都能够获取构造函数的原型对象,也就能拿到该示例中的getValue()函数.
手写new:
javascript
function mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
备注: 如果你没有使用 new
运算符,构造函数会像其他的常规函数一样被调用,并不会创建一个对象。在这种情况下, this
的指向也是不一样的。
忘了在哪里搜集的图片了,非常非常全面:
js
- 构造函数生成实例对象`person`,`person`的`__proto__`指向构造函数`Person`原型对象
- `Person.prototype.__proto__` 指向内置对象,因为 `Person.prototype` 是个对象,默认是由 ` Object `函数作为类创建的,而 `Object.prototype` 为内置对象
- `Person.__proto__` 指向内置匿名函数 `anonymous`,因为 Person 是个函数对象,默认由 Function 作为类创建
- `Function.prototype` 和 ` Function.__proto__ `同时指向内置匿名函数 `anonymous`,这样原型链的终点就是 `null`
Function.__proto__
ƒ () { [native code] } //指向图中的function anonymous
Function.__proto__.__proto__ //指向了内置对象
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, ...}constructor: ƒ Object()hasOwnProperty: ƒ hasOwnProperty()isPrototypeOf: ƒ isPrototypeOf()propertyIsEnumerable: ƒ propertyIsEnumerable()toLocaleString: ƒ toLocaleString()toString: ƒ toString()valueOf: ƒ valueOf()__defineGetter__: ƒ __defineGetter__()__defineSetter__: ƒ __defineSetter__()__lookupGetter__: ƒ __lookupGetter__()__lookupSetter__: ƒ __lookupSetter__()__proto__: (...)get __proto__: ƒ __proto__()set __proto__: ƒ __proto__()
Function.__proto__.__proto__.__proto__
null //指向null
Function.prototype //prototype同理
ƒ () { [native code] }
Function.prototype.__proto__
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, ...}
Function.prototype.__proto__.__proto__
null
每个对象的__proto__
都是指向它的构造函数的原型对象prototype
的
ini
person1.__proto__ === Person.prototype
构造函数是一个函数对象 ,是通过 Function
构造器产生的
ini
Person.__proto__ === Function.prototype
原型对象本身是一个普通对象 ,而普通对象的构造函数都是Object
javascript
Person.prototype.__proto__ === Object.prototype
刚刚上面说了,所有的构造器都是函数对象,函数对象都是 Function
构造产生的
javascript
Object.__proto__ === Function.prototype
Object
的原型对象也有__proto__
属性指向null
,null
是原型链的顶端
javascript
Object.prototype.__proto__ === null
下面作出总结:
- 一切对象都是继承自
Object
对象,Object
对象直接继承根源对象null
- 一切的函数对象(包括
Object
对象),都是继承自Function
对象 Object
对象直接继承自Function
对象Function
对象的__proto__
会指向自己的原型对象,最终还是继承自Object
对象
实现继承
下面给出JavaScripy
常见的继承方式:
- 原型链继承
- 构造函数继承(借助 call)
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
原型链继承:
js
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child2';
}
Child.prototype=new Parent() //可以看看new时发生了什么
let s1=new Child()
let s2=new Child()
s1.play.push("add")
s1.play
(4) [1, 2, 3, 'add']
s2.play
(4) [1, 2, 3, 'add']
我们会发现这种写法存在很大的问题,当修改s1中的属性时 s2也会同步修改,这是因为paly是定义在原型上的,创建的s1和s2实例是公用当前的原型的,也就是说通过访问__proto__去修改原型上的数据,其他实例在访问原型上的属性时都会访问到被修改的值:
js
s1.__proto__.name
'parent1'
s1.__proto__.name='s1'
's1'
当s2去访问时:
s2.name
's1'
当我们使用实例去设置值时,例如:
js
s1.name='s1'
// **会在当前对象添加name 如果需要修改原型中name 需要使用__proto__去修改**
1. Child {type: 'child2', name: 's1'}
1. 1. name: "s1"
1. type: "child2"
1. [[Prototype]]: Parent
1. 1. name: "parent1"
1. play: (4) [1, 2, 3, 'add']
1. [[Prototype]]: Object
构造函数继承
借助call去调用parent函数:
js
function Parent2(){
this.name="parent2"
this.getName=function () {
return this.name
}
}
Parent1.prototype.say = function () {}
function Child2() {
Parent2.call(this)
this.type="child2"
}
console.log(new Child2())
构造函数借助call实现继承,Parent2.call(this)中,this指向Child2,这种方式的缺点是只能继承父类实例中的属性和方法,不能继承父类原型中的属性和方法。
组合继承
将以上两种方式进行组合实现:
js
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this);
this.type = 'child3';
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
第一次调用Parent3:将child3的原型对象链接到parent3的原型上 这样可以访问parent3原型上的方法 第二次调用Parent3:实在构造Child3实例时,继承到Parent3实例上的属性和方法 缺点就是Parent3调用了两次,会造成性能浪费
原型继承
通过Object.create()实现普通对象的继承
Object.create()
静态方法以一个现有对象作为原型,创建一个新对象。
lua
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
person4.friends.push("jerry");person5.friends.push("lucy");
都会顺着原型链进行查找,进而找到parent4:
js
person5.__proto__===parent4
true
parent4和parent5都会向上查找到friends,对其修改后会影响到所有的子类,所以再访问时会拿到修改后的值。
寄生式继承
js
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
可以添加一些方法,但是还是使用的同一个对象作为原型,创建的实例可以沿着原型链去修改原型中的变量,从而影响所有的后面的实例去访问原型中的变量。
寄生组合式继承
js
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5
寄生组合式继承使用call方式继承父类的实例对象(通过this,)的属性和方法,Parent6.call(this);
this指向Child6,并在Parent6原型对象上 绑定方法,再通过Object.create()方法创建原型对象并赋值给Child6(通过create方法创建完原型并赋值后需要指定constructor构造函数为Child6,否则构造函数为继承的parent6),
- 通过call方法去继承父类this上的属性和方法
- 通过create方法继承父类原型上的属性和方法
- 创建的child实例对象是相互独立的
js
person6.play.push(11)
console.log(person6);
console.log(person7);