理解原型链
JavaScript在设计之初,作为一种网页脚本语言,没有设计得很复杂,这种语言只要能够完成一些简单操作就够了。Javascript里面所有的数据类型都是对象(object)。在ES6之前,js中是没有Class的概念的(ES6中的类也是语法糖,本质还是基于原型),为了实现实例对象的属性和方法共享,就给function设计了一个prototype的概念。每个对象都有一个原型属性,原型属性指向另一个对象,而原型属性中的对象是会被其他对象所继承的,也就是共用的,对象自身的有些属性和方法则不可以。原型对象(prototype)也有一个自己的原型对象(__proto__),普通属性并非为对象。
实例对象的隐式原型指向其构造函数的显示原型
prototype和__proto__
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
prototype:
函数
的一个属性- 是个对象
- 创建函数时会默认添加propertype这个属性
__proto__:
对象
的一个属性:- Object.prototype的__proto__属性是一个访问器属性(一个getter函数和一个setter函数)
- 指向构造函数prototype
- 公开访问它的对象的内部[[Prototype]](对象或null)
function ptt() {
this.a = 1
}
const obj = new ptt()
// 函数:
console.dir(ptt)
console.log(ptt.prototype)
// 既然prototype是个对象,那么就有__proto__
console.log(ptt.prototype.__proto__)
console.log(ptt.prototype.__proto__ === Object.prototype)// true
// 对象:
console.dir(obj)
// 既然是对象,那么就有__proto__
console.log(obj.__proto__)
console.log(obj.__proto__ === ptt.prototype)//true
console.log(Object.prototype.__proto__)//null
ptt.prototype.b = 2
Object.prototype.c = 3
console.log(ptt.prototype.b)// 2
console.log(obj.c)// 3
以上代码生成的原型链结构如下:
obj {
a:1
__proto__:ptt.prototype = {
b:2,
__proto__:Object.prototype = f{
c:3
__proto__:null
}
}
}
在Chrome浏览器的开发者工具(F12)打印console.dir(obj)
后如图:
实际上,Chrome 控制台打印的 [[Prototype]]
即表示对象的原型,并不需要一层层的去寻找__proto__:
难点在于……构造函数也是对象!
如果没有 prototype,那么共用属性就没有立足之地,
prototype 指向一块内存,这个内存里面有共用属性;
__proto__ 指向同一块内存;
如果没有__proto__,那么一个对象就不知道自己的共用属性有哪些。上面所说__proto__属性是一个访问器属性,通俗来说,prototype就是用__proto__来查找有哪些对象是可继承,且继承了的。
- prototype存储共用的属性和方法,
- __proto__用来将对象与该对象的原型相连,
- constuctor是用来将原型对象指向关联的构造函数。
原型链
通过原型链从而找到toString()
方法,而其他对象同样具有此方法,所以并不是对象会把这些方法和属性复制过来,只是通过原型(prototype)找到被继承的对象的原型。
当我们创建一个对象后,就可以通过“点”方法名的方式调用一些并不是我们写的方法了,例如:
let num=123
num.toString()
console.log(num.toString === Number.prototype.toString); // true
console.log(Object.getPrototypeOf(num) === Number.prototype); // 输出: true
其实我们调用的是Number.prototype.toString。现在是不是对JavaScript是基于原型的语言这句 话有些理解了。
其实不止Number是这样的,Array、String、Object等都是这样的原理。
“JavaScript-一切皆对象”
构造函数的prototype属性与通过该构造函数创建的对象实例的原型是同一个对象,因此Object.getPrototypeOf(new Foobar()) === Foobar.prototype
。
原型的实践
create()
可以使用Object.create()的方法,创建原型对象,例如:
let person = {
name: 'Rarrot'
};
var person1 = Object.create(person);
console.log(person1.__proto__);
创建了一个新的对象 person1,它的原型被设置为之前定义的 person 对象。通过 Object.create(person),person1 继承了 person 的所有属性和方法。
注意
,虽然使用 __proto__ 属性来直接访问一个对象的原型在很多情况下都能工作,但这并不是推荐的做法,因为它并不是所有JavaScript环境都支持的标准特性。更标准的方式是使用 Object.getPrototypeOf()
方法来获取一个对象的原型。例如:
console.log(Object.getPrototypeOf(person1));// { name: 'Rarrot' }
constructor属性
每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于该构造此实例对象的构造函数,例如:
//如果构造此对象的是person函数,返回person(),没有的话就返回Object()
console.log(person1.constructor); // ƒ Object() { [native code] }
如果想要创建一个新的实例,但是拿不到实例的引用,可以使用这个属性来找到构造函数,并使用 new 关键字来创建一个新的实例:
var person3 = new person1.constructor('Rarrot2');
修改原型
通过在构造函数的 prototype 上定义方法,可以实现一个强大的继承机制,其中通过这个构造函数创建的所有对象实例都可以访问这些方法。例如,假设有一个构造函数 Person,可以为它的原型添加一个 joke 方法:
function Person(firstName) {
this.name = { first: firstName };
}
Person.prototype.joke = function () {
alert(this.name.first + ' ahhh!');
};
var person = new Person('Rarrot');
person.joke(); // 弹出信息 "Rarrot ahhh!"
在这个例子中,任何由 Person 构造函数创建的实例(如 person3)都可以调用 joke 方法。
证明了先前的原型链模型这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。
一种常见的定义模型
在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。
// 构造器及其属性定义
function Test(a,b,c,d) {
// 属性定义
};
// 定义第一个方法
Test.prototype.x = function () { ... }
// 定义第二个方法
Test.prototype.y = function () { ... }
典型的例子:school plan app
力扣
可以去力扣上找设计题目类型的题来理解原型链,例如: