前言
这篇文章,我们有三个目标:
- 原型存在,是为了解决什么问题
- 对象属性查找的过程
- 对象,原型对象 和构造函数 的三者之间的关系
在文章的阅读过程中,心中装着这三个问题,读起来才有目标,才更有效率
我们先看一个代码:
const person = {};
console.log(person.toString()); // [object Object]
person 是一个空对象,其中没有任何属性,但是在下面又调用toString属性,toString 属性从哪里来的?
肯定不是来自 person对象的。
来自哪里呢?是来自 person 的原型对象的!
在下面图中,在控制台打印出了 person 对象的结构:
可以看到里面一个属性也没有,但是有一个[[Prototype]]属性,这个属性指向一个对象,我们打开这个属性看看:
这里正好有一个 toString。那代码中的person.toString()是不是来自这里呢?
要解释这个问题,得知道什么是原型对象
原型
概念: 在 Javascript 中,原型对象是用于实现继承和共享属性的机制。每个 Javascript 对象都有一个原型对象,通过原型链可以访问和继承原型对象上的属性和方法
这里回答了文章开头第一个问题
上面有两个关键地方:
- 原型对象是用于继承和属性共享的
- 每个 JavaScript 对象都有一个原型对象
继承和属性的共享
继承有什么意义呢,继承是为了实现属性的复用。在python(面向对象)中,实现对象继承有什么方法?
用class:
class Animal:
def __init__(self, name):
self.name = name
def get_name(self):
return self.type
class Dog(Animal):
def __init__(self):
super().__init__(name)
dog = Dog('Dog');
print(dog.get_name()); // dog
而js是用什么实现继承呢,用原型。
下面来看一段代码:
const person = {
name: 'zenos'
};
person.toString(); // [object, Object]
在上面的person对象中,并没有toString属性,为什么可以访问呢?是因为 person 通过原型继承了另一个对象
深入原型
原型是 JavaScript 中实现继承的一种机制。每个对象都有一个内部属性 [[Prototype]],指向它的原型对象。
通过 [[Prototype]] 属性,一个对象可以访问其原型及其原型对象的属性和方法。在对象中查找属性的时候,会从当前对象查找,如果当前对象没有,就会从当前对象的原型对象中查找。
上面的 person 空对象中没有 toString 属性,但 person 能够调用这个方法,就是因为 person 对象的原型对象上有 toString。
你看,每个创建出来的对象,并不用手动添加 toString 属性,但是都可以使用 toString,这不就是继承吗?
[[prototype]]属性指向的是对象的原型对象,但在实际代码中,这样写是会报错的。
如果真的在代码中写
person[[prototype]],表示什么意思?
在js 实际开发中,是通过__proto__属性来访问原型对象的。
我们来看一段代码:
const person = {
name: "zenos",
};
person.__proto__.otherName = 'blue';
上面的代码给 person 的原型对象添加了一个otherName的属性,看看控制台:
原型对象上确实多了otherName的属性。
可以像普通属性一样正常访问:
复杂的例子
下面的例子通过__proto__对 person 对象的原型对象进行覆盖
const person = {
name: "zenos",
};
person.greet = function() {
console.log("person");
};
// 覆盖person的原型
person.__proto__ = {
greet: function(){
console.log("person.__proto__");
},
greetInner: function () {
console.log("person.__proto__");
},
};
// 覆盖person的原型的原型
person.__proto__.__proto__ = {
greetInner: function () {
console.log("person.__proto__.__proto__");
},
};
person.greet();
person.greetInner();
console.log(person.toString())
person.greetInner2();
在控制台看看 person 的结构,然后回答下面的问题:
- 代码中的
person.greet()调用结果是什么?
因为 greet 方法是直接定义在 person 对象上的,因此调用 person.greet() 时,会执行 person 对象自身的 greet 方法,而不会去查找原型链。
- 代码中的
person.greetInner()调用结果是什么?
因为greetInner 方法没有在 person 对象上直接定义,因此 JavaScript 会沿着原型链查找。person.__proto__ 上定义了 greetInner 方法,所以调用 person.greetInner() 时,会执行 person.__proto__ 上的 greetInner 方法。
person.toString()会正常执行吗?
会!
因为 person.proto.proto 的原型对象上有toString属性
person.greetInner2()会正常执行吗?
会报错!
因为 person 及其原型对象,原型的原型,原型的原型都没有greetInner2属性,那么person.greetInner2得到的是undefined,对undefined执行调用操作,即undefined(),会报错!
小结
从上面的代码中,可以得出几个结论:
- 在对象中查找属性的时候,会从
当前对象查找,如果当前对象没有,就会从当前对象的原型对象中查找。 - 如果对象及其原型对象有相同的属性,优先用当前对象的属性。
如果一直没有找到属性,会一直查下去吗?
不会!当查找到[[prototype]]的值不再是对象,而是 null 的时候,就停止查找,然后返回undefined
上面就是对象属性查找的规则。
这里回答了文章开头的第二个问题
我们会发现查找的过程中,会涉及到多个原型对象,而原型对象之间是由__proto__([[prototype]])链接来的,我们把这个链条,叫做原型链。
回顾person 的结构:
图形版的原型链:
有没有醍醐灌顶的感觉呢😄
深入原型,实例对象和函数的关系
function Fn(name) {
this.name = name;
}
const obj = new Fn('zenos');
console.log(obj); // { name: 'zenos'}
Fn 是一个构造函数,通过 Fn 创建了一个对象 obj。很简单,我们把 obj 称为构造函数 Fn 的实例对象
构造函数有一个属性prototype,这个属性指向一个对象 A,并且对象 A 会成为构造函数实例对象的原型对象
也就是说 obj 的原型对象是对象 A。用代码表示:
console.log(obj.__proto__ === Fn.prototype); // true
概念辨析
__proto__、[[prototype]]、prototype这三个属性,有什么关系?
__proto__对应着对象的原型对象,是每个对象都会有的属性。查找属性的时候,如果没有查找到,就会去原型对象上找。__proto__和[[prototype]]是同一个东西,只不过__proto__是代码开发的表示方式,[[prototype]]是控制台打印时表示的方式
prototype 属性只存在于函数中,这个属性指向的对象会成为对应实例对象的原型对象
- 构造函数和普通函数有什么区别?
没什么区别,当普通函数被 new 关键词调用的时候,就称为构造函数。注意箭头函数不能被作为构造函数,即不能被 new 操作。
并且**对象 A **上有一个constructor的属性,这个属性指向构造函数本身
console.log(Fn.prototype.constructor === Fn); // true
有点乱?没关系,我们画个图,就很清晰了
这幅图回答了文章开头的第三个问题
这是 obj 在控制台的打印结构:
偷偷告诉你一个小秘密,对象 A 虽然是 obj 的原型对象,但对象 A 也是对象啊,所以它也有自己的原型对象🤭
console.log(Fn.prototype.__proto__); // ? 试试打印的结果
既然对象 A 会成为 Fn 实例对象的原型对象,那我直接修改对象 A,不就相当于直接修改了 Fn 实例对象的原型对象么
Fn.prototype.hello = function(){
console.log('hello');
}
const obj2 = new Fn('zenos2');
console.log(obj2.name);
obj2.hello();
上面代码执行结果如下:
符合你的预期吗?😼
看看 obj2 的结构:
有了构造函数,我们可以真正地使用继承了!
初试继承
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.getName = function () {
return this.name;
};
Animal.prototype.getAge = function () {
return this.age;
};
const animal = new Animal('tom',2);
上面有个构造函数 Animal,创建了一个实例对象 animal:
现在我想创建一个 Dog 对象,Dog 的实例对象也有 name,age 两个属性,原型对象上也有 getName,getAge 两个方法,怎么做?
function Dog(name, age) {
this.name = name;
this.age = age;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.getAge = function () {
return this.age;
};
这是最简单的做法,其实 Dog 可以继承 Animal:
function Dog(name, age) {
// 给 dog 实例对象添加name,age属性
Animal.call(this, name, age);
}
// 让Dog的实例对象和Animal实例对象的原型对象一致
Dog.prototype = Animal.prototype;
看看效果:
const dog = new Dog('dogName',3);
打印看看 dog 的结构:
该有的属性都有了。这就是继承 Animal 的方式。
当然这种方式比较简陋,其中还有一些小问题,不过没关系,这些小问题我们以后都会解决。这里只需要对继承有一个了解即可
总结
这篇讲了原型存在的目的、对象属性寻找逻辑,还通过原型,我们看到了构造函数和对象的关系。不难,下篇文章我们深入了解原型链