深入理解 JavaScript 中的原型与对象
在 JavaScript 中,原型是一项强大的概念,贯穿于对象创建、继承、和属性访问的方方面面。本文将深入讨论原型及相关概念,从显式原型、隐式原型到原型链,为你揭示 JavaScript 中这个核心特性的奥秘。
javascript
function Person() {
this.name = '小明'
this.age = 18
}
Person.eat = function() {
console.log('想吃面条')
}
let p = new Person()
let p2 = new Person()
console.log(p === p2)//false
以上代码中,构造函数 Preson,当我们new了两个Person,得到两个对象。在之前的学习中,我们知道了函数是引用类型,存在于堆中,当我们拿到它,其实是拿到了它在堆中的地址。按理说第11行语句的结果是true才对,说明,每new一次得到的对象都是新的对象。
第5行代码我们可以看到,我们往Person挂了一个eat属性(函数也是对象),执行Person.eat()是可以调用的,但当我们去查看new出来的Person对象时发现它并不在里面,也就是说,p.eat()是会报错的。(我的浏览器没有识别出 '小明' -.-')我们如何去解决这个问题呢?往下看
1. 显式原型
1.1 定义
在 JavaScript 中,每个函数都有一个特殊的属性,称为 prototype
。这个属性定义了由该函数创建的对象的公共祖先。通过该构造函数创建的对象,可以隐式地继承这个原型上的属性和方法。
1.2 意义
显式原型允许我们将共享的属性和方法定义在一个地方,从而简化代码。所有由同一构造函数创建的对象,都共享一个原型,节省内存并使代码更加可维护。
1.3 原型上的属性修改
需要注意的是,原型上的属性修改只能由原型自己操作,实例对象无权修改。这一点对于理解 JavaScript 的继承机制至关重要。
2. 隐式原型
2.1 定义
对象原型,通常称为隐式原型,是实例对象的 __proto__
属性,指向构造函数的显式原型。
2.2 关系
实例对象的隐式原型与构造函数的显式原型相互关联。通过这种关系,实例对象可以访问构造函数原型上的属性和方法。
ini
<script>
//Car.prototype.name = 'BMW'
//Car.prototype.lang = 4900
//Car.prototype.height = 1400
Car.prototype = {
name: 'BMW',
lang: 4900,
height: 1400
}
function Car(owner, color) {
// this.name = 'BMW'
// this.lang = 4900
// this.height = 1400
this.owner = owner
this.color = color
}
let car = new Car('小帅', 'red')
let car2 = new Car('小美', 'pink')
console.log(car);
</script>
以上代码我们可以看到,Car.prototype
这就是往这个构造函数的显示原型里添加属性。所以当我们new 一个Car时,它会隐式继承该原型的属性和方法。所以当我们要修改原型属性,也要通过该种方法
也就是说car.__proto__===Car.prototype
3. new 操作符
3.1 创建过程
使用 new
操作符创建对象的过程包含以下步骤:
- 创建一个空对象。
- 执行构造函数中的逻辑,将
this
绑定到新创建的对象上。 - 将新对象的隐式原型指向构造函数的显式原型。
- 返回新创建的对象。
new
操作符的机制保证了新对象与构造函数原型之间的正确关联。
4. 原型链
4.1 定义
原型链是由对象的隐式原型一级一级连接起来的链条。在属性访问时,JavaScript 引擎会沿着原型链向上查找,直到找到目标属性或者到达原型链的末端(null)。
4.2 每个对象都有一个隐式原型:
在 JavaScript 中,每个对象都有一个 __proto__
属性,它指向该对象的原型。这个原型对象也是一个对象,并且它也有自己的 __proto__
,形成了一条由对象连接而成的链,即原型链。
4.3. 原型链的顺序查找:
当你访问一个对象的属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或者到达原型链的末端(null
)。
4.4. 原型链的形成:
原型链是通过对象的隐式原型一级一级连接起来的。一个对象的隐式原型指向构造函数的显式原型,而构造函数的显式原型也是一个对象,它的隐式原型又指向另一个构造函数的显式原型,依此类推,形成了原型链。
4.5. 构造函数和原型链:
每个构造函数都有一个显式原型对象,通过 prototype
属性访问。新创建的对象通过 new
操作符与构造函数关联,其隐式原型指向构造函数的显式原型。
下面是一个简单的例子来演示原型链:
javascript
// 构造函数
function Animal(name) {
this.name = name;
}
// 在构造函数的原型上添加方法
Animal.prototype.sayName = function() {
console.log("My name is " + this.name);
};
// 创建实例对象
const myAnimal = new Animal("Leo");
// 访问实例对象的方法
myAnimal.sayName(); // 输出: My name is Leo
// 实例对象的隐式原型指向构造函数的显式原型
console.log(myAnimal.__proto__ === Animal.prototype); // 输出: true
// 构造函数的显式原型的隐式原型指向 Object 构造函数的显式原型
console.log(Animal.prototype.__proto__ === Object.prototype); // 输出: true
// Object 构造函数的显式原型的隐式原型是 null
console.log(Object.prototype.__proto__ === null); // 输出: true
在这个例子中,myAnimal
实例对象通过原型链可以访问到 sayName
方法,而原型链的末端是 null
,表示查找到此处为止。
5. 网易面试题解析
5.1 所有对象都有隐式原型吗?
在 JavaScript 中,几乎所有的对象都有隐式原型,但有一种特殊情况例外。通过 Object.create(null)
创建的对象是没有隐式原型的。
javascript
const obj = Object.create(null);
console.log(obj.__proto__); // 输出: undefined
在这个例子中,通过 Object.create(null)
创建的对象 obj
不继承任何属性和方法,它的隐式原型是 undefined
,而不是指向标准的对象原型(Object.prototype
)。
这种情况通常用于创建一个纯净的、不继承任何属性和方法的对象,可以完全按照你的需求定义其属性和方法。然而,这也意味着这个对象失去了一些 JavaScript 常规对象的特性,比如无法使用一些内置的方法和属性。因此,使用 Object.create(null)
需要慎重考虑,确保你真的需要一个没有原型链的对象。
结语
深刻理解原型、隐式原型、new
操作符和原型链是成为 JavaScript 高效开发者的关键。这些概念贯穿了 JavaScript 的对象模型,对于正确理解和利用这门语言至关重要。希望通过本文的讲解,读者能够更清晰地理解 JavaScript 中原型的精髓。