1.认识原型对象
1.1 认识对象的原型
JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。
那么这个对象有什么用呢?
- 当我们通过引用对象的属性key来获取一个value时,它会**触发 [[Get]]**的操作;
- 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;
- 如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
- 答案是有的,只要是对象都会有这样的一个内置属性;
获取的方式有两种:
-
方式一:通过对象的 _proto_ 属性可以获取到(但是这个是早期浏览器自己添加的, 是非标准的,存在一定的兼容性问题);
var obj = { name: "why", age: 18, height: 1.88, }; console.log(obj.__proto__)
-
方式二:通过 Object.getPrototypeOf 方法可以获取到(标准的);
var obj = { name: "why", age: 18, height: 1.88, }; console.log(Object.getPrototypeOf(obj))
2.2 认识函数的原型
那么我们知道上面的东西对于我们的构造函数创建对象来说有什么用呢?
- 它的意义是非常重大的,接下来我们继续来探讨;
这里我们又要引入一个新的概念:所有的函数都有一个prototype的属性(注意:不是_proto_)
function foo() {
}
//将函数看成一个普通的对象时, 他是具备__proto__(又叫做隐式原型 或 对象原型)
console.log(foo.__proto__)
// 将函数看成是一个函数时, 所有的函数中都有一个属性, 名字是prototype(又叫显式原型)
console.log(foo.prototype)
你可能会有疑惑,是不是因为函数是一个对象,所以它有prototype的属性呢?
-
不是的,因为它是一个函数,才有了这个特殊的属性;
-
而不是它是一个对象,所以有这个特殊的属性;
var obj = { name: "kaisa", age: 18 } // 对象中是没有prototype属性的 console.log(obj.prototype) // undefined
小结: 对象和函数的原型作用是什么
- 对象原型的作用: 查找key对应的value时, 会找到原型身上
- 函数原型的作用: 用来构建对象时, 将这个显式原型赋值给创建出来对象的隐式原型(这句话有点抽象, 我们继续向下学习)
2.函数的原型作用
2.1 new操作原型的赋值
我们前面讲过new关键字的步骤如下:
- 在内存中创建一个新的对象(空对象);
- 这个对象内部的[[prototype]](__proto__隐式原型)属性, 会被赋值为该构造函数的prototype(显式原型)属性;
- 将this指向这个空对象
- 执行函数体中的代码
- 如果没有明确返回一个对象, 那么就将这个对象默认返回
那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype:
function Person() {
}
var p1 = new Person()
// 上面操作相当于会进行如下的操作
p1.__proto__ = Person.prototype
function Person() {}
var p1 = new Person();
var p2 = new Person();
console.log(p1.__proto__ === p2.__proto__); // true
console.log(p1.__proto__ === Person.prototype); // true
2.2 将方法放在原型上
我们先来看下面这个案例:
- 使用构造函数创建三个学生: stu1, stu2, stu3
- 每创建一个学生, 都会创建三个新的不同的函数(方法)
- 如果创建一百个学生, 就会创建对应的100 * 3个不同的函数, 这样是比较浪费性能的
function Student(name, age, id) {
this.name = name;
this.age = age;
this.id = id;
this.running = function () {
console.log(this.name + "running");
};
this.eating = function () {
console.log(this.name + "eating");
};
this.studying = function () {
console.log(this.name + "studying");
};
}
// 创建三个学生
var stu1 = new Student("coder", 18, 001);
var stu3 = new Student("kaisa", 19, 002);
var stu2 = new Student("vn", 16, 003);
console.log(stu1.eating === stu3.eating); // false
- 当我们多个对象拥有共同的值时, 我们可以将它放到构造函数的显式原型上, 由构造函数创建出来的所有对象, 都会共享这些属性
- 当我们调用方法时, 会优先实例自身上去查找, 没有找到的话就会去原型上查找
- 而实例的**_proto_(隐式原型)会指向Student的prototype(显式原型)**
function Student(name, age, id) {
this.name = name;
this.age = age;
this.id = id;
}
// 将函数放在原型上
Student.prototype.running = function() {
console.log(this.name + "running");
}
Student.prototype.eating = function() {
console.log(this.name + "eating");
}
Student.prototype.studying = function() {
console.log(this.name + "studying");
}
// 创建三个学生
var stu1 = new Student("coder", 18, 001);
var stu3 = new Student("kaisa", 19, 002);
var stu2 = new Student("vn", 16, 003);
console.log(stu1.eating === stu3.eating); // true
2.3 constructor属性
事实上函数的原型对象上面是有一个属性的:constructor
-
默认情况下函数的原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;
function Student(name) { this.name = name } // 创建一个学生 var stu1 = new Student("coder"); console.log(Student.prototype.constructor === Student); // true // stu1.__proto__ === Student.prototype console.log(stu1.__proto__.constructor); // [Function: Student] console.log(stu1.__proto__.constructor.name); // Student
2.4 内存的表现图
2.4.1 constructor内存表现
- 函数原型中有一个constructor属性, constructor指向Person(当前的函数对象)这个构造函数
- 构造函数的实例中, 也有一个相同的constructor属性, 同样指向Person(当前的函数对象)这个构造函数
- prototype添加属性
2.4.2 创建对象内存表现
创建对象时的内存表现
对应代码:
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log("running~")
}
var p1 = new Person("why", 18)
var p2 = new Person("kobe", 30)
// 进行操作
console.log(p1.name) // why
console.log(p2.name) // kobe
p1.running() // running
p2.running() // running
对应画图:
原型中新增属性时的内存表现
对应代码:
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log("running~")
}
var p1 = new Person("why", 18)
var p2 = new Person("kobe", 30)
// 进行操作
console.log(p1.name)
console.log(p2.name)
p1.running()
p2.running()
// 新增属性
Person.prototype.address = "中国"
p1.__proto__.info = "中国很美丽!"
p1.height = 1.88
p2.isAdmin = true
// 获取属性
console.log(p1.address) // 中国
console.log(p2.isAdmin) // true
console.log(p1.isAdmin) // undefined
console.log(p2.info) // 中国很美丽
// 修改address
p1.address = "广州市" // 并不会修改函数的原型中address, 而是在p1中添加一个address属性
console.log(p2.address) // 由于函数的原型中address并没有被修改, 所以p2的address依然是中国
对应画图:
2.5 重写函数原型对象
如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象:
function Person() {
}
// 添加的属性
Person.prototype.message = "Hello Person";
Person.prototype.address = "成都市";
Person.prototype.running = function () {
console.log("running~~~");
};
var p1 = new Person();
p1.running(); // running~~~
function Person() {
}
// 重写函数对象
Person.prototype = {
message: "Hello Person",
address: "成都市",
running: function() {
console.log("running~~~")
}
}
var p1 = new Person();
p1.running(); // running~~~
前面我们说过, 每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性;
- 而我们这里相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函数, 而不是Person构造函数了
如果希望constructor指向Person,那么可以手动添加:
function Person() {
}
// 重写函数对象
Person.prototype = {
message: "Hello Person",
address: "成都市",
running: function () {
console.log("running~~~");
},
constructor: Person, // 手动添加constructor指向Person
};
console.log(Object.keys(Person.prototype)); // ['message', 'address', 'running', 'constructor']
var p1 = new Person();
p1.running(); // running~~~
上面的方式虽然可以, 但是也会造成constructor的[[Enumerable]]特性被默认设置了true.
- 默认情况下, 原生的constructor属性是不可枚举的.
- 如果希望解决这个问题, 就可以使用我们前面介绍的Object.defineProperty()函数了.
function Person() {}
// 重写函数对象
Person.prototype = {
message: "Hello Person",
address: "成都市",
running: function () {
console.log("running~~~");
}
};
// 手动将constructor改为不可枚举, 返回值为Person
Object.defineProperty(Person.prototype,"constructor", {
enumerable: false,
value: Person
})
console.log(Object.keys(Person.prototype)); // ['message', 'address', 'running']
var p1 = new Person();
p1.running(); // running~~~
3.原型链和继承
3.1 面向对象的特性 – 继承
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
- 多态:不同的对象在执行时表现出不同的形态;
这里我们核心讲继承, 那么继承是做什么呢?
- 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;
- 在很多编程语言中,继承也是多态的前提;
那么JavaScript当中如何实现继承呢?
- 不着急,我们先来看一下JavaScript原型链的机制;
- 再利用原型链的机制实现一下继承;
3.2 JavaScript原型链
在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。
-
我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:
var obj = { name: "kaisa", age: 18 } // 相当于自定义了一条原型链 obj.__proto__ = { } obj.__proto__.__proto__ = { } obj.__proto__.__proto__.__proto__ = { address: "成都市" } console.log(obj.address) // 成都市
那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性呢?
console.log(obj.__proto__.__proto__.__proto__.__proto__); // [Object: null prototype] {}
我们会发现它打印的是 [Object: null prototype] {}
- 事实上这个原型就是我们最顶层的原型了
- 从Object直接创建出来的对象的原型都是 [Object: null prototype] {}
原型链的关系内存图
3.3 原型链实现方法的继承
如果我们现在需要实现继承,那么就可以利用原型链来实现了:
-
目前让Student的原型指向p这个实例对象,而p对象的隐式原型是Person的显式原型,里面包含running等函数;
-
实现步骤:
- 定义构造函数Person类, 作为父类
- 向构造函数Person类的显式原型中添加方法(父类的方法)
- 定义构造函数Student类, 作为子类
- 创建父类对象的实例, 并作为子类的显式原型对象
- 向构造函数Student类的显式原型中添加方法(子类的方法)
-
注意:步骤4和步骤5不可以调整顺序,否则会有问题
// 1.定义构造函数Person类, 作为父类 function Person(name, age) { this.name = name; this.age = age; } // 2.向构造函数Person类的显式原型中添加方法(父类的方法) Person.prototype.running = function () { console.log("running~"); } // 3.定义构造函数Student类, 作为子类 function Student(name, age, id, score) { this.name = name; this.age = age; this.id = id; this.score = score; } // 4.创建父类对象的实例, 并作为子类的显式原型对象 var p = new Person("chen", 19); // 此时p的隐式原型指向Person的显式原型 Student.prototype = p; // 让Student的显式原型指向父类对象的实例p // 5.向构造函数Student类的显式原型中添加方法(子类的方法) Student.prototype.studying = function () { console.log("studying"); }; // 创建学生 var stu1 = new Student("coder", 18, 001, 100); var stu2 = new Student("kaisa", 20, 002, 95); // 已经基本实现了继承 stu1.running(); // 可以调用父类方法 stu2.studying(); // 可以调用子类方法 console.log(stu1.name, stu1.age); // 可以访问子类的属性 console.log(stu2.id, stu2.score); // 可以访问子类的属性
对应的内存表现图:
但是目前我们实现的继承有一个很大的弊端:某些属性其实是保存在p对象上的;
- 第一,我们通过直接打印对象是看不到这个属性的;
- 第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
- 第三,不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没办法定制化);
3.4 借用构造函数属性继承
上面我们实现的继承, 父类和子类中有相同的属性(重复代码)
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之
为经典继承或者称之为伪造对象):
- steal是偷窃、剽窃的意思,但是这里可以翻译成借用;
借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数.
- 因为函数可以在任意的时刻被调用;
- 因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;
// 1.定义构造函数Person类, 作为父类
function Person(name, age, height, address) {
this.name = name;
this.age = age;
this.height = height;
this.address = address;
}
// 2.定义构造函数Student类, 作为子类
function Student(name, age, height, address, id, score) {
// this.name = name;
// this.age = age;
// this.height = height;
// this.address = address;
// 重点, 借用构造函数属性继承
// 调用Person,使用call方法为Person绑定this(这里的this是指向Student的实例), 并传入参数
Person.call(this, name, age, height, address);
this.id = id;
this.score = score;
}
3.5 组合借用继承的问题
组合继承是ES6之前JavaScript最常用的继承模式之一(将原型链继承和借用构造函数组合):
- 如果你理解到这里, 点到为止, 那么组合来实现继承只能说问题不大;
- 但是它依然不是很完美,但是基本已经没有问题了;
组合继承存在什么问题呢
- 组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。
- 一次在创建父类的实例作为子类的显式原型的时候;
- 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
- 另外,如果你仔细按照我的流程走了上面的每一个步骤,你会发现:所有的子类实例事实上会拥有两份父类的属性
- 一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是person.__proto__里面);
- 当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;
小结: 其实组合式继承已经基本没有什么问题, 已经可以使用, 但是用父类创建出来的实例§作为子类的显式原型是非常” 别扭 “的, 接下来我们会详细介绍如何优化它(在社区通过许多人的努力, 优化了很多个版本)
3.6 继承优化的方案
3.6.1 原型式继承函数
原型式继承的渊源
- 这种模式要从**道格拉斯·克罗克福德(**Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的一篇文章说起: Prototypal Inheritance in JavaScript(在JavaScript中使用原型式继承)
- 在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的.
- 为了理解这种方式,我们先再次回顾一下JavaScript想实现继承的目的:重复利用另外一个对象的属性和方法.
最终的目的:student对象的原型指向了person对象, 而我们只是需要一个中间对象, 将父类子类连接起来;
这个中间对象需满足以下条件:
- 必须创建出来一个对象
- 这个对象的隐式原型必须指向父类的显式原型
- 将这个对象赋值给子类的显式原型(这个对象作为子类的显式原型)
以前的方案:
function Person() {}
function Student() {}
// 以前的做法: 但是不想要这种做法
var p = new Person();
Student.prototype = p;
方案一: 创建一个对象, 再给这个对象的隐式原型绑定到父类的显式原型上, 以及将这个对象绑定到子类的显式原型上
function Person() {}
function Student() {}
var obj = {};
// obj.__proto__ = Person.prototype // 前面说过__proto__有兼容性问题, 编写逻辑时尽量不使用
Object.setPrototypeOf(obj, Person.prototype) // 尽量使用标准的语法
Student.prototype = obj;
方案二: 创建一个新的构造函数
function Person() {}
function Student() {}
function Func() {}
// 将Func的显式原型绑定到Person的显式原型上
Func.prototype = Person.prototype
// 让Student的显式原型对象绑定为Func构造函数的的实例对象, 这个实例对象的隐式原型指向Func的显式对象
Student.prototype = new Func()
方案三: 使用Object.create()方法
Object.create()方法创建一个新的对象, 并且第一个参数: 是指向这个新对象的原型的指向
function Person() {}
function Student() {}
// 创建一个新对象obj, 且使obj的原型指向父类Person的显式原型
var obj = Object.create(Person.prototype);
Student.prototype = obj;
3.6.2 寄生式继承函数
寄生式(Parasitic)继承
- 寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推
广的; - 寄生式继承的思路是结合原型类继承和工厂模式的一种方式;
- 即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;
封装函数: 将我们前面的方案, 封装成可以传入父类和子类自动实现继承
function Person() {}
function Student() {}
function inherit(Subtype, Supertype) {
var obj = Object.create(Supertype.prototype);
Student.prototype = obj;
// Student.prototype = Object.create(Supertype.prototype); // 可以合成一行代码
}
我们知道显式对象都有constructor属性, 再完善一下constructor属性的设置
function Person() {}
function Student() {}
function inherit(Subtype, Supertype) {
Subtype.prototype = Object.create(Supertype.prototype);
// 完善constructor
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype,
});
}
如果担心Object.create()有兼容性, 我们可以封装一个自己的Object.create()函数
function createObject(obj) {
// 创建一个构造函数Func
function Func() {}
// 让构造函数实例的原型指向传入的obj
Func.prototype = obj;
// 通过new就会让实例对象的隐式原型绑定到obj上
return new Func();
}
将上面两个封装的函数利用起来就是,寄生式继承
3.7 继承最终方案: 寄生组合式
现在我们来回顾一下之前提出的比较理想的组合继承
- 组合继承是比较理想的继承方式, 但是存在两个问题:
- 问题一: 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候.
- 问题二: 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中.
事实上, 我们现在可以利用寄生式继承将这两个问题给解决掉.
- 你需要先明确一点: 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中的属性和方法复
制一份到了子类型中. 所以父类型本身里面的内容, 我们不再需要. - 这个时候, 我们还需要获取到一份父类型的原型对象中的属性和方法.
- 能不能直接让子类型的原型对象 = 父类型的原型对象呢?
- 不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改.
- 我们使用前面的寄生式思想就可以了.
我们最终的继承实现方案: 我们使用了借用构造函数继承、原型链、原型式和寄生式函数多种方法组合而成, 所以叫做寄生组合式
- 借用构造函数用于继承属性, 寄生式用于继承方法
**寄生组合式示例代码: **
// 1.定义构造函数Person类, 作为父类
function Person(name, age, height) {
this.name = name;
this.age = age;
this.height = height;
}
Person.prototype.running = function () {
console.log("running~");
};
Person.prototype.eating = function () {
console.log("eating~");
};
// 3.定义构造函数Student类, 作为子类
function Student(name, age, height, id, score) {
// 使用借用构造函数继承属性
Person.call(this, name, age, height);
// 子类自己的属性
this.id = id;
this.score = score;
}
// 使用我们刚刚封装的两个函数
function createObject(obj) {
// 创建一个构造函数Func
function Func() {}
// 让构造函数实例的原型指向传入的obj
Func.prototype = obj;
// 通过new就会让实例对象的隐式原型绑定到obj上
return new Func();
}
function inherit(Subtype, Supertype) {
Subtype.prototype = createObject(Supertype.prototype);
// 完善constructor
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype,
});
}
// 调用函数实现继承即可
inherit(Student, Person);
// 可以向子类原型中中添加自己方法
Student.prototype.studying = function () {
console.log("studying");
};
// 创建Student实例
var stu1 = new Student("coder", 16, 1.68, 110, 119);
var stu2 = new Student("kaisa", 18, 1.88, 111, 120);
console.log(stu1); // {name: 'coder', age: 16, height: 1.68, id: 110, score: 119}
console.log(stu2); // {name: 'kaisa', age: 18, height: 1.88, id: 111, score: 120}
stu1.running(); // 可以调用子类的方法
stu2.eating(); // 可以调用父类的方法
stu1.studying(); // 可以调用子类的方法
3.8 Object是所有类的父类
从我们上面的Object原型我们可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FrXbpbRc-1655258999071)(E:\xiazai\coderwhy\笔记\js高级\img\Snipaste_2022-06-14_20-03-00.png)]
4.对象判断方法补充
hasOwnProperty
-
对象是否有某一个属于自己的属性(不是在原型上的属性)
var obj1 = { name: "kaisa", age: 18, }; var obj2 = { address: "成都市", info: "一座来了不想走的城市", }; obj2.__proto__ = obj1; console.log(obj2.name); // 访问原型对象中的name console.log(obj2.hasOwnProperty("name")); // false console.log(obj2.hasOwnProperty("address")); // true
in/for in 操作符
-
判断某个属性是否在某个对象或者对象的原型上
var obj1 = { name: "kaisa", age: 18, }; var obj2 = { address: "成都市", info: "一座来了不想走的城市", }; obj2.__proto__ = obj1; console.log("name" in obj2) // true console.log("info" in obj2) // true
var obj1 = { name: "kaisa", age: 18, }; var obj2 = { address: "成都市", info: "一座来了不想走的城市", }; obj2.__proto__ = obj1; // for...in 不仅遍历自身的, 还遍历原型上的 for(key in obj2) { console.log(key[obj2]) // address info name age }
instanceof
-
用于检测构造函数(Person、Student类)的pototype,是否出现在某个实例对象的原型链上
function Person() {} function Student() {} // 调用前面封装继承的函数 inherit(Student, Person); var stu = new Student(); console.log(stu instanceof Student); // true console.log(stu instanceof Person); // true console.log(stu instanceof Object); // true console.log(stu instanceof Array); // false
isPrototypeOf
-
用于检测某个对象,是否出现在某个实例对象的原型链上, 可以判断对象之间的继承
function Person() {} function Student() {} // 调用前面封装继承的函数 inherit(Student, Person); var stu = new Student(); // 判断Student的原型对象是否在实例对象stu的原型链上 console.log(Student.prototype.isPrototypeOf(stu)); // true // 判断Person的原型对象是否在实例对象stu的原型链上 console.log(Person.prototype.isPrototypeOf(stu)); // true
isPrototypeOf
-
用于检测某个对象,是否出现在某个实例对象的原型链上, 可以判断对象之间的继承
function Person() {} function Student() {} // 调用前面封装继承的函数 inherit(Student, Person); var stu = new Student(); // 判断Student的原型对象是否在实例对象stu的原型链上 console.log(Student.prototype.isPrototypeOf(stu)); // true // 判断Person的原型对象是否在实例对象stu的原型链上 console.log(Person.prototype.isPrototypeOf(stu)); // true
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/120139.html