目录
1. 理解原型
const device = {
powerSource: "battery",
displayDevice: true
}
const phone = { size: "small" }
const huaweiPhone = { os: "HarmonyOS" }
// Object.setPrototypeOf需要两个对象作为参数,
// 它将第二个对象设置为第一个对象的原型
Object.setPrototypeOf(phone, device);
// 可以直接访问原型的属性
console.log(phone.powerSource);
// battery
Object.setPrototypeOf(huaweiPhone, phone);
// 也可以直接访问原型的原型的属性,即原型是链状的
// 访问属性时自下而上层层查找
console.log(huaweiPhone.displayDevice);
// true
// 用in操作符判断某个对象是否具有某个属性
console.log("size" in huaweiPhone);
// true
2. 对象构造器与原型
- 每一个函数都有一个原型对象,该原型对象将被自动设置为通过该函数创建的对象的原型
2.1 实例与原型
function Students(){}
// 在对象的原型对象上添加属性
Students.prototype.study = function() {
return true;
}
// 通过new作为构造函数调用,新建实例
const stu1 = new Students();
// 实例拥有了原型对象的属性
console.log("study" in stu1);
// true
// 直接调用函数
const stu2 = Students();
console.log("study" in stu2);
// Uncaught TypeError: Cannot use 'in' operator to search for 'study' in undefined
- 每个函数都具有一个原型对象
- 每个函数的原型都有一个constructor属性,该属性指向函数本身
graph LR;
Students.prototype.constructor-->Students
- constructor对象的原型设置为新创建的对象的原型
graph LR;
stu1-.new.->Students.prototype.constructor.prototype=Students.prototype
2.2 实例属性:初始化过程的优先级
// 在实际代码中不推荐这么做
function Student() {
this.gender = "Male";
// 实例方法与原型方法同名
this.sayHi = function() {
return "Hi!";
}
}
// 原型方法与实例方法同名
Student.prototype.sayHi = function() {
return "I refuse to.";
}
const stu = new Student();
// 实例会隐藏(并不是替换)原型中与实例方法重名的方法
// 在构造函数内部,关键字this指向新创建的对象
// 所以在构造函数内添加的属性直接在新的实例上
// 查找时是先查找实例内部再查找原型中
console.log(stu.sayHi());
// Hi!
在函数的原型上创建对象方法可以使一个方法由所有对象实例共享
2.3 JS动态特性的副作用:通过原型,一切都可以在运行时修改
function Student(){
this.type = "Primary";
}
const a1 = new Student();
// 在原型上添加方法
Student.prototype.getInfo = function(){
return this.type;
}
// 实例可以正常访问
console.log(a1.getInfo());
// Primary
// 将原型指向另一个对象,并在对象里定义方法
Student.prototype = {
getName: function() {
return "Classified";
}
}
// 之前实例化的对象依旧可以访问原来的方法
console.log(a1.getInfo());
// Primary
// 但不能使用新对象的方法
console.log(a1.getName());
// Uncaught TypeError: a1.getName is not a function
// 重新实例化一个对象
const a2 = new Student();
// 可以使用新方法了
console.log(a2.getName());
// Classified
// 但不能使用之前原型上的方法
console.log(a2.getInfo());
// Uncaught TypeError: a2.getInfo is not a function
对象与函数原型之间的引用关系是在对象创建时建立的,原型对象指向的改变并不会影响改变前就已经实例化的对象,即函数的原型可以被任意替换,但已经构建的实例引用旧的原型
2.4 通过构造函数实现对象类型
- 通过constructor属性访问创建该对象所用的函数,这个特性可以用于类型校验
function Student(){}
const a1 = new Student();
// 通过typeof只能得知a1是个对象
console.log(typeof a1);
// object
// 通过instanceof只能得知a1是Student的实例
console.log(a1 instanceof Student);
// true
// 通过对象的constructor属性可以得到其构造函数引用
console.log(a1.constructor === Student);
// true
// 所以可以用constructor属性来创建对象
const a2 = new a1.constructor();
console.log(a2 instanceof Student);
// true
可以用constructor属性创建对象,即使原始构造函数已经不再作用域内。但如果重写了constructor属性,那么原始值就会丢失了
3. 实现继承
3.1 子类的原型是父类的实例实现继承
function People() {}
People.prototype.dance = function() {
console.log("Dancing");
}
function Student() {}
// 子类的原型是父类的实例
Student.prototype = new People();
const a1 = new Student();
// 可以调用
a1.dance();
// Dancing
console.log(a1 instanceof Student);
// true
console.log(a1 instanceof Object);
// true
console.log(a1 instanceof People);
// true
使用People的原型对象作为Student的原型,即
People.prototype = Student.prototype
也可以实现继承,但强烈不建议使用。
缺点:People原型上发生的所有变化都被同步到Student原型上
优点:所有继承函数的原型将实时更新
3.2 重写constructor属性的问题
原型的重写导致constructor指向父类
function People() {}
People.prototype.dance = function() {
console.log("Dancing");
}
function Student() {}
// 子类的原型是父类的实例,但也重写了原型对象,导致后面的问题
Student.prototype = new People();
const a1 = new Student();
// a1的constructor属性指向了父类
console.log(a1.constructor === People);
// true
console.log(a1.constructor === Student);
// false
配置对象的属性
属性描述 | 作用 |
---|---|
configurable | true: 可以删除或修改 false: 不允许修改 |
enumerable | true: 可以被for-in遍历到 |
value | 指定属性的值,默认为undefined |
writable | true: 可以通过赋值语句修改属性值 |
get | 默认为undefined 定义getter函数,当访问属性时发生调用 不能与value和writable同时使用 |
set | 默认为undefined 定义setter函数,当设置属性时发生调用 不能与value和writable同时使用 |
const book = {}
book.name = "Ninja";
book.price = 99.00;
// 内置方法配置属性,接收三个参数:目标对象,属性,配置(对象表示)
Object.defineProperty(book, "content", {
configurable: false,
enumerable: false,
value: "Classified",
writable: true
});
// 属性成功添加,可以访问
console.log("content" in book);
// true
// content属性的enumerable设置为false,所以for-in无法遍历
for(let prop in book) {
console.log(prop);
}
// name
// price
解决constructor属性被覆盖的问题
function People() { }
People.prototype.dance = function () {
console.log("Dancing");
}
function Student() { }
// 子类的原型是父类的实例,但也重写了原型对象,导致constructor属性被覆盖
Student.prototype = new People();
// 注意要配置的constructor属性是在原型上的
Object.defineProperty(Student.prototype, "constructor", {
enumerable: false,
value: Student // 让constructor重新指向原来的构造器
});
const a1 = new Student();
console.log(a1.constructor === People);
// false
console.log(a1.constructor === Student);
// true -> constructor指向正确
3.3 instanceof操作符:基于原型链的检测
function Student() { }
const a1 = new Student();
// 在原型链中
console.log(a1 instanceof Student);
// true
// 修改原型
Student.prototype = {};
// a1已经不在Student的原型链中
console.log(a1 instanceof Student);
// false
4. 在ES6中使用JS的class
// class关键字定义类
class People{
// 定义构造函数
constructor(name) {
this.name = name;
}
// 定义实例方法
toString() {
return this.name;
}
}
// extends关键字实现继承
class Student extends People{
constructor(name, age) {
// super关键字用于访问和调用父类上的静态方法
// 在构造函数中出现时,必须在使用this关键字之前使用
super(name); // 调用父类的构造函数
this.age = age;
}
sayHi() {
return "Hi!";
}
// static关键字定义静态方法
static compare(stu1, stu2) {
return stu1.age - stu2.age;
}
}
const stu1 = new Student("Wango", 24);
// 实例化对象有效
console.log(stu1 instanceof Student);
// true
// 继承有效
console.log(stu1 instanceof People);
// true
const stu2 = new Student("Lily", 25);
// 调用静态方法有效
console.log(Student.compare(stu1, stu2));
// -1
// 调用父类方法有效
console.log(stu1.toString());
// Wango
console.log(stu2.toString());
// Lily
ES6引入关键字class,但是底层依然是基于原型实现,以上代码基本等价于一下代码,但要注意下列代码中原型继承的缺陷
function People(name) {
this.name = name;
}
People.prototype.toString = function() {
return this.name;
}
function Student(age) {
this.age = age;
}
Student.prototype.sayHi = function() {
return "Hi!";
}
Student.compare = function(stu1, stu2) {
return stu1.age - stu2.age;
}
// 使用原型继承的一大缺陷:
// 向父类传递的参数会成为所有子类实例的初始化数据
Student.prototype = new People("Wango");
Object.defineProperty(Student.prototype, "constructor", {
enumerable: false,
value: Student
});
const stu1 = new Student(24);
// 实例化对象有效
console.log(stu1 instanceof Student);
// true
// 继承有效
console.log(stu1 instanceof People);
// true
const stu2 = new Student(25);
// 调用静态方法有效
console.log(Student.compare(stu1, stu2));
// -1
// 调用父类方法
console.log(stu1.toString());
// Wango
console.log(stu2.toString());
// Wango
// 给父类的参数成了初始化数据,原型继承有缺陷,还有其他几种继承方式本书暂未介绍