原型 (Prototype)
# 原型 (Prototype)
在 JavaScript 中,原型是一种用于实现对象继承和共享数据的机制。每个 JavaScript 对象(除了 null
)都与另一个对象相关联,这个对象就是原型,每个对象都从原型“继承”属性和方法。
prototype
是构造函数的一个属性,其属性值为对象,称为原型对象。- 可以通过
prototype
来添加新的属性和方法,此时所有由该构造函数创建的对象都会具有这些属性和方法。 - 由该构造函数创建的对象会默认链接到该原型对象上。
# 1. 构造函数与原型
构造函数用于创建对象。每个构造函数都有一个 prototype
属性,这个属性指向一个对象,这个对象包含了由构造函数创建的所有实例共享的属性和方法。
语法:
- 添加属性:
构造函数.prototype.属性名 = 值;
- 添加方法:
构造函数.prototype.方法名 = function() { 方法体 };
// 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 向原型添加方法
Person.prototype.sayHello = function() {
console.log(`你好,我叫 ${this.name},我 ${this.age} 岁。`);
};
// 创建对象
var person1 = new Person("张三", 25);
var person2 = new Person("李四", 30);
person1.sayHello(); // 输出:你好,我叫 张三,我 25 岁。
person2.sayHello(); // 输出:你好,我叫 李四,我 30 岁。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
访问对象属性的查找顺序
当访问一个对象的属性时,JavaScript 引擎会按照以下顺序查找:
- 首先在当前对象中查找对应的实例属性。
- 如果没有找到,就会到该对象关联的构造函数的
prototype
属性中查找,即到原型对象中查找。 - 如果在原型对象中也没有找到,则继续沿着原型链向上查找,直到找到顶层的
Object.prototype
。 - 如果最终仍未找到,则返回
undefined
。
原型的作用
- 对象间共享数据:通过在原型中定义属性和方法,可以使多个对象实例共享这些属性和方法,减少内存消耗。
- 为类(系统内置或自定义)增加新的属性和方法:可以动态地向原型添加方法,并且新增内容对于当前页面中已经创建的对象也有效。
# 2. proto
prototype
是构造函数的属性,是站在构造函数的角度来讨论其原型属性。__proto__
是对象的属性,是站在对象的角度来讨论其原型对象。__proto__
属性指向构造函数的prototype
对象。- 注:由于
__proto__
是非标准属性,因此一般不建议使用。
示例:
console.log(person1.__proto__ === Person.prototype); // 输出:true
console.log(Person.prototype.constructor === Person); // 输出:true
console.log(Object.getPrototypeOf(person1) === Person.prototype); // 输出:true
2
3
# 3. 对象的类型
# 3.1 判断数据的类型
- 使用
typeof
可以判断任意变量的类型,但判断对象的类型时总是返回object
。 - 使用
instanceof
判断对象是否为某种类型,需要指定判断的目标数据类型,无法获取对象的类型名称。
console.log(typeof person1); // 输出:object
console.log(person1 instanceof Person); // 输出:true
console.log(person1 instanceof Object); // 输出:true
2
3
# 3.2 获取对象的类型
每个 JavaScript 对象(除了 null
和 undefined
)都有一个 constructor
属性,指向它的构造函数。通过 constructor
属性可以获取构造函数,从而获取对象的类型名称。
// 定义一个构造函数 Student
function Student() {}
// 使用构造函数创建一个对象 stu
var stu = new Student();
// 通过对象的 constructor 属性获取构造函数,并通过构造函数的 name 属性获取类型名称
console.log(stu.constructor.name); // 输出:Student
2
3
4
5
6
兼容性问题
需要注意的是,在代码压缩或混淆的情况下,构造函数的名称可能会被改变,因此依赖 constructor.name
获取对象类型的方式可能会失效。
# 4 constructor 属性
- 每个对象都有一个
constructor
属性,该属性指向其构造函数。 - 对象的
constructor
属性是其原型对象提供的,因为每个对象都链接到其原型对象上。
// 定义一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 创建一个对象实例
var person1 = new Person("张三", 25);
// 使用 constructor 属性
console.log(person1.constructor); // 输出:Person
console.log(person1.constructor === Person); // 输出:true
2
3
4
5
6
7
8
9
10
11
12
# 5. 原型链
- 任何对象都有其原型对象,原型对象也有原型对象,对象的原型对象一直往上找,直到
null
为止。这一过程称为原型链。 - 在这一过程中,有一个 Object 类型的对象,它就是
Object.prototype
,位于原型链的顶层。
这个图示展示了 JavaScript 中对象的原型链结构和 __proto__
、prototype
、constructor
之间的关系。以下是对图中内容的解释:
构造函数 Object:
- JavaScript 中的所有对象都是通过构造函数创建的。图中表示了构造函数
Object
。 Object
构造函数有一个prototype
属性,指向Object.prototype
对象。
- JavaScript 中的所有对象都是通过构造函数创建的。图中表示了构造函数
Object.prototype:
Object.prototype
是一个对象,包含了构造函数Object
的原型属性和方法。Object.prototype
具有一个constructor
属性,指向Object
构造函数本身。这意味着通过Object.prototype
创建的所有对象都共享Object
构造函数。Object.prototype
还有一个__proto__
属性,指向它的原型。由于Object.prototype
是原型链的顶端,其__proto__
属性指向null
。
创建了对象 o1:
- 使用
Object
构造函数创建一个对象o1
。 - 对象
o1
具有一个__proto__
属性,指向它的原型对象Object.prototype
。
- 使用
解释图中每个元素
- Object:构造函数,用于创建对象。
- prototype:构造函数的属性,指向构造函数的原型对象。
- proto:对象的属性,指向对象的原型。
- constructor:原型对象的属性,指向构造函数本身。
示例代码
// 构造函数
function Person(name) {
this.name = name;
}
// 创建对象
var person1 = new Person('张三');
// 原型链关系
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
2
3
4
5
6
7
8
9
10
11
12
13
- 当创建
person1
对象时,person1.__proto__
指向Person.prototype
。 Person.prototype.constructor
指向Person
构造函数。Person.prototype.__proto__
指向Object.prototype
,这表示Person.prototype
的原型是Object.prototype
。Object.prototype.__proto__
指向null
,表示原型链的终点。
总结
- 原型链是 JavaScript 中实现继承和共享数据的机制。
- 通过原型链,对象可以继承和共享原型上的属性和方法。
- 通过
constructor
属性可以获取对象的构造函数,通过__proto__
属性可以访问对象的原型,理解原型链的结构有助于深入理解 JavaScript 的继承机制。
# 6. call 和 apply
# 6.1 作用
call
和 apply
方法都是 JavaScript 中用于改变函数调用上下文(this
指向)的方法。通过这两个方法,我们可以以某个对象的身份来调用另一个对象的方法,从而改变 this
的指向。
# 6.2 语法
call
:方法.call(对象, 参数1, 参数2...)
apply
:方法.apply(对象, [参数数组])
# 6.3 区别
call
是逐个传参,后面参数可以有多个,逗号隔开。apply
是以数组形式传参,后面参数只能有一个,会自动拆分为多个元素传入。
// 定义一个函数
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
// 创建一个对象
var person = { name: "张三" };
// 使用 call 方法调用 greet 函数,将 this 指向 person 对象
greet.call(person, "你好", "!"); // 输出:你好, 张三!
// 使用 apply 方法调用 greet 函数,将 this 指向 person 对象
greet.apply(person, ["你好", "!"]); // 输出:你好, 张三!
2
3
4
5
6
7
8
9
10
11
12
13
适用场景
call
方法:适用于参数数量已知且较少的情况,可以逐个列出所有参数。apply
方法:适用于参数数量不确定或参数以数组形式存在的情况,方便传递参数数组。
# 7. 继承
# 7.1 对象冒充继承(构造继承)
使用 call
,以对象冒充的形式调用父类的构造函数,相当于是复制父类的实例属性给子类,但只能继承父类构造函数中的属性和方法,无法继承原型中的属性和方法。
// 父类构造函数
function Parent(name) {
this.name = name;
}
// 子类构造函数
function Child(name, age) {
// 通过 Parent.call 调用父类构造函数,继承父类属性
Parent.call(this, name);
this.age = age;
}
var child = new Child("李四", 10);
console.log(child.name); // 输出:李四
console.log(child.age); // 输出:10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 7.2 原型链继承
使用 prototype
,将父类的实例作为子类的原型。通过这种方式,子类可以继承父类原型中的方法。
// 父类构造函数
function Parent(name) {
this.name = name;
}
// 在父类的原型上添加方法
Parent.prototype.sayHello = function() {
console.log(`你好,我叫 ${this.name}`);
};
// 子类构造函数
function Child(name, age) {
// 调用父类构造函数
this.name = name;
this.age = age;
}
// 将父类的实例作为子类的原型
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child = new Child("李四", 10);
child.sayHello(); // 输出:你好,我叫 李四
console.log(child.age); // 输出:10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 7.3 组合继承
组合对象冒充继承和原型链继承的优点,既可以继承父类构造函数中的属性和方法,也可以继承原型中的属性和方法。
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ["红色", "蓝色", "绿色"];
}
// 在父类的原型上添加方法
Parent.prototype.sayHello = function() {
console.log(`你好,我叫 ${this.name}`);
};
// 子类构造函数
function Child(name, age) {
// 通过 Parent.call 调用父类构造函数,继承父类属性
Parent.call(this, name);
this.age = age;
}
// 将父类的实例作为子类的原型
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child1 = new Child("李四", 10);
child1.colors.push("黄色");
console.log(child1.colors); // 输出:[ '红色', '蓝色', '绿色', '黄色' ]
child1.sayHello(); // 输出:你好,我叫 李四
var child2 = new Child("王五", 20);
console.log(child2.colors); // 输出:[ '红色', '蓝色', '绿色' ]
child2.sayHello(); // 输出:你好,我叫 王五
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30