1.前言
我们要探索 JavaScript 中的一个超酷的技术------原型。原型是编程中的一种强大工具,它能让你的代码更加高效和精巧。
想象一下,原型就像是编程的蓝图,可以帮助我们构建出更强大的对象。通过了解原型,我们能够更深入地理解 JavaScript 中的对象结构,让我们的代码变得更为优雅和可维护。
在这个学习之旅中,我们将深入了解原型的概念,探索如何利用原型链创建更灵活的代码结构。这并不是什么神秘的技巧,而是实实在在的编程智慧,能够让你的代码更加强大和高效。
2.原型的概念
2.1 函数的原型(显式原型、原型对象)
在 JavaScript 中,每个函数都有一个特殊的属性,称为 prototype
。这个 prototype
是一个对象,它包含了一个指向原型对象的引用。
原型是函数天生就具有的属性。它定义了构造函数制造出的对象的公共祖先。通过该构造函数产生的对象,可以隐式继承到原型上的属性和方法。
js
function myFunction() {
// 函数体
}
console.log(myFunction.prototype); // 输出: {}
这里,myFunction.prototype
就是 myFunction
函数的显示原型。
显示原型的作用
函数的原型对象是一个普通的对象,但它对于实现继承和共享属性和方法至关重要。让我们看看它的作用:
1.继承
当你创建一个对象实例时,它会继承其构造函数的原型。这样,通过原型链,实例可以访问原型对象中定义的属性和方法。
js
function Person(name) {
this.name = name
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`)
}
let person1 = new Person('dante')
person1.sayHello(); // 输出:Hello, I'm dante
2 共享属性和方法
所有通过相同构造函数创建的实例都共享同一个原型对象。这意味着,如果你修改了原型对象,所有实例都会受到影响。
js
Person.prototype.age = 18;
console.log(person1.age); // 输出: 18
let person2 = new Person('dante');
console.log(person2.age); // 输出: 18
prototype 与 proto
要注意 prototype
是函数特有的属性,而 __proto__
则是实例对象特有的属性。__proto__
指向其构造函数的原型对象。
js
console.log(person1.__proto__ === Person.prototype); // 输出: true
虽然 __proto__
是一种访问原型链的方式,但并不推荐使用,因为它不是标准的 JavaScript API。推荐使用 Object.getPrototypeOf()
方法来获取对象的原型。
js
console.log(Object.getPrototypeOf(person1) === Person.prototype); // 输出: true
构造函数的 prototype
构造函数本身也有一个 prototype
属性,它与实例的 __proto__
指向同一个对象。这个构造函数的 prototype
用于定义实例对象的原型。
js
console.log(Person.prototype === myFunction.prototype); // 输出: true
函数原型在 JavaScript 中扮演着重要的角色,它通过原型链实现了继承和共享属性和方法的机制。深入理解函数原型有助于更好地利用 JavaScript 中的面向对象编程特性。
2.2对象的原型(隐式原型)
在 JavaScript 中,每个对象都有一个原型。原型可以看作是对象的父对象,它包含了对象共享的属性和方法。当我们访问一个对象的属性或方法时,JavaScript 引擎会首先在对象本身查找,如果找不到,就会去原型中查找。
现在我们通过具体的代码来了解什么是对象原型,使用构造函数是一种创建对象的常见方式。构造函数可以看作是一种特殊的函数,通过 new 关键字调用时,它会创建一个新的对象,并将该对象的原型指向构造函数的原型。
js
Person.prototype.say = function (){
return this.name + '今年' + this.age + '岁了'
}
function Person(){
this.name = 'dante'
this.age = 18
}
const p = new Person()
console.log(p) // Person {name: 'dante', age: 18}
console.log(p.say()) //dante今年18岁了
从上面的代码,我们可以看到,Person构造函数中并没有say()这样一个方法,但是为什么实例p可以访问say这个方法呢?这就是在创建实例p的时候,p继承了构造函数Person的显式原型中的方法,在js引擎查找这个say()方法时,首先在实例p中查找say,没有就会在p的原型上查找,刚好,实例p的原型继承了构造函数Person中的属性和方法。我总结了以下两点:
-
当访问对象属性时,先找对象显式具有的属性,没找到再去找对象的隐式原型。
-
实例对象的隐式原型 === 构造函数的显式原型
2.3原型链
从上面我们了解到了函数的显式原型和对象的隐式原型吗,明白了这两个概念,原型链就很好解决了,那什么是原型链呢?
当一个对象中的方法被调用执行时,引擎查找的时候首先会查找对象的显式具有的属性,没有,就会在对象原型中去查找,若还没有找到,就会继续向构造该对象的构造函数的显式原型中查找,还未找到,继续向上一层查找,一直到null。
顺着对象的隐式原型不断地向上查找上一级的隐式原型,直到找到目标或者一直到null,这种查找关系叫做原型链
3.所有的对象都有隐式原型吗?
了解了对象原型,那我们来思考这个问题,所有的对象都有隐式原型吗?文章读到这里,好像这句话说的非常的正确。 但是有一个特例,通过Object.create(null)创建的对象是没有隐式原型的。
js
// Object.create()
let obj = {
a:1
}
let obj2 = Object.create(null)
console.log(obj)
console.log(obj2)
上面是在浏览器上打印obj的结果,是有Prototype的,而下方是打印obj2的结果,没有Prototype
Object.create(null)
创建的对象之所以为空(即没有原型链上的属性和方法),是因为它的原型被显式地设置为 null
。
在 JavaScript 中,Object.create()
方法接受一个参数,用于指定新创建对象的原型。当你传入 null
作为参数时,创建的对象将没有原型链,也就是说它不继承任何属性或方法。
示例:
js
const emptyObject = Object.create(null);
console.log(emptyObject.toString); // 输出: undefined
console.log(emptyObject instanceof Object); // 输出: false
在这个例子中,emptyObject
被创建为一个空对象,它不具备任何继承的属性或方法。toString
属性为 undefined
,而且它不是 Object
的实例,因为它没有原型链。
通常,这种操作被用于创建一个"纯净"的、不继承任何属性或方法的对象,以防止不必要的属性干扰。这样的对象可以用于一些特定的用途,比如作为映射(Map)的初始值,因为它不会受到原型链上其他属性的影响。
4. 总结
函数原型和对象原型共同构成了 JavaScript 中强大的原型链系统。它们通过原型链的连接关系,实现了继承、属性共享,为 JavaScript 提供了灵活且强大的面向对象编程能力。深入理解这两个概念,可以帮助你更好地设计和组织代码,使其更具可维护性和可扩展性。