开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情
目标
- 理解对象
- 理解对象创建变化过程
- 理解原型
- 理解原型链
顺着下文加粗的问题X就可以理解为什么这样改进
ES6之前如何实现类
类是用于创建对象的模板。他们用代码封装数据以处理该数据。JS 中的类建立在原型上——MDN
在ES6之后,JavaScript 才正式支持了类和继承的语法,ES6之前是没有类的写法,而是通过原型实现的类。
实际上,类是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。看懂了下面的过程,就知道ES6之前如何实现类的了
理解对象
对象的字面定义:一组属性的无序集合
可以把对象想成一张散列表,其中的内容就是key:value 其中value可以是数据也可以函数,如下代码所示,name,age这些就是key
let person = {
name : "xiaoming";
age : 20;
job : "student";
sayName(){
console.log(this.name);
}
}
理解对象创建变化过程
在ES6之前创建对象的三种方法:
- 利用对象字面量创建
let person = {
name : "xiaoming"; //person对象属性1
age : 20; //person对象属性2
job : "student"; //person对象属性3
sayName(){
console.log(this.name);
}
}
- 利用new Object()创建
let person = new Object(); //创建了一个名为person的对象
person.name = "xiaoming"; //person对象属性1
person.age = 20; //person对象属性2
person.job = "student"; //person对象属性3
person.sayName = function() //person对象方法
{
console.log(this.name);
}
虽然使用对象字面量和new Object可以方便的创建对象,但是这两种方式也存在明显不足,如果要创建100个这样的对象呢?用for?即使可以用,但是会消耗大量的内存空间放重复的东西,比如这个地方的sayName(问题1)
即使利用工厂模式解决多次创建多个对象的问题,但不能明确的是:新创建的对象是什么类型?(问题2)
而ECMAScript的构造函数适用于创建特定类型的对象(如 Array等)那么我们也可以自定义,创建类似Person这样的对象
- 利用构造函数创建对象
function Person(name,age,job)
{
this.name = name;
this.age = age;
this.job = job;
this.sayName(){
console.log(this.name);
}
}
var xiaoming = new Person(xiaoming,18,student);
var xiaohong = new Person(xiaohong,20,student);
构造函数和普通函数没有区别,唯一的区别就是调用的方式不同 看下面三个例子:
let obj = new Person(xiaoming,18,student); //用new作为构造函数调用
obj.sayName(); // 会打印出“xiaoming”
Person(xiaoming,18,student); //不加new作为普通函数调用
window.sayName(); // 不加指定的this会默认添加到window上去(浏览器中,node中是global)
console.log("下一步学习可以继续研究一下this的指向问题")
构造函数中的属性和方法我们统称为成员,这些成员是可以添加的
实例成员就是构造函数内部通过this添加的成员(如name,age,job,sayName()就是实例成员)
实例成员只能通过实例化的对象来访问 ,不可以通过构造函数来访问 如:Person.name是访问不到的(这个地方我记得和数据属性的四个特性有关?)
静态成员就是在构造函数本身身上添加的成员 如:Person.sex = "男"
静态成员只能通过构造函数来访问 ,不可以通过对象来访问
理解原型
由此,构造函数看似已经解决了上述的问题1和问题2
但是构造函数也不是没有问题,有什么问题呢?
我们知道,在Person里声明定义sayName()方法的时候,它作为函数,而在ECMA中的函数是对象(数据类型为:引用数据类型),会在堆中重新开辟地址来存放,造成多少个对象就会浪费多少个相同函数的内存(问题3)
如下代码所示,在调用sayName()的时候明明都是实现同样的逻辑却要占用三份相同的内存
var obj1 = new Person(xiaoming,18,student);
var obj2 = new Person(zhangsan,30,worker);
var obj3 = new Person(lisi,40,free);
obj1.sayName();
obj2.sayName();
obj3.sayName();
所以,引入了在构造函数中引入原型prototype,而这个prototype就是用来解决问题3的。(这种方法也叫原型模式创建对象)
我们向构造函数中添加一个prototype属性,而这个属性里面装的是什么呢?就是类似sayName()一类的公共方法
(但也不是说prototype上只能放方法,name这类属性也可以放,只要是共同不变的对象,避免重新开辟内存都可以放在上面)
prototype这个属性在构造函数Person中的形式是prototype:{ object }
(可以自己打印出来看看)
(prototype翻译过来是原型,并且 { } 代表是一个对象,所以叫原型对象)
由此,我们可以把那些不变的方法直接定义在原型对象上,这样所有的实例对象都会共享这些方法不需要再开辟内存空间
function Person(name,age,job)
{
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
var obj1 = new Person(xiaoming,18,student);
var obj2 = new Person(zhangsan,30,worker);
var obj3 = new Person(lisi,40,free);
obj1.sayName();
obj2.sayName();
obj3.sayName();
阶段总结1:js规定,只要是创建一个函数(刚已经说了构造函数和普通函数都是函数)里面就有一个prototype的属性,这个prototype里面装的是公共的属性和方法
再来,方法定义在构造函数身上,为什么实例化的对象能访问到?也就是说obj1是如何和Person.prototype.sayName()建立的连接?(问题4)
因为实例化对象的身上有一个对象原型 __proto__或者[[prototype]]
阶段总结2:同样,js规定每个对象都有一个属性__proto__指向构造函数的prototype(比如{}.__proto__=== Object.prototype),之所以我们对象可以使用构造函数上的属性和方法都是因为__proto__的存在
obj1.__proto__<===> Person.prototype (显示的是object) 两者等价true
(Person.prototype.__proto__ === Object.prototype)
但其实这样写还是有点问题,什么问题呢?如果我这个公共的方法很多(注释掉的部分),看起来也不太美观(问题5),我们就采取对象的方式打包起来直接赋值给Person的prototype
//自有属性写在构造函数里面
function Person(name,age,job)
{
this.name = name;
this.age = age;
this.job = job;
}
//Person.prototype.constructor = Person 任何一个非箭头函数的prototype.constructor都等于它本身
//Person.prototype.sayName = function(){
//console.log(this.name);
//}
//Person.prototype.sayJob = function(){
//console.log(this.job);
//}
//共有属性写在原型上面
Person.prototype = {
//constructor:Person; 没有这一句的话
sayName : function(){
console.log(this.name);
},
sayJob : function(){
console.log(this.job);
},
}
var obj1 = new Person(xiaoming,18,student);
console.log(Person.prototype);//这个时候打印出来就没constructor属性了,取而代之的是一个对象
console.log(obj.__proto__);
但如果我们修改了原型对象的写法,给原型对象赋值的是一个对象而不是一个函数,在这种情况下,console.log(Person.prototype) 打印出来,取而代之的是一个对象
带来的问题就是没有原型,他俩到底是谁的孩子(之前提到的问题2),就不清楚了 所以我们需要手动添加constructor属性指回原来的构造函数Personconstructor:Person
阶段总结3:如果我们修改了原型对象的写法,给原型对象赋值的是一个对象而不是一个函数,则必须手动的利用constructor属性指回原来的构造函数Person
结合上面的三个阶段总结就可以看懂这张图了
这也保证了我们能顺着原型链找到成员
有没有感受到面向对象的封装继承多态的味道了,有了原型链的形式我们就可以继续提出继承,在此基础上继承要如何实现呢?ES6之后正式支持面向类和继承,它又是如何实现的呢?
ES6之后如何实现类
其实说简单一点就是语法糖,进行了封装,这是一个用ES6语法定义类的基本示例,可以看到没有了prototype
class Person {
//自有属性写在constructor里面
constructor(name, age) {
this.name = name;
this.age = age;
}
//共有属性写在constructor外面
sayHello() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
}
const person = new Person('John', 25);
person.sayHello();
在上面的示例中,我们还是声明了一个名为 Person
的类。类的构造函数 constructor
用于初始化对象的属性,sayHello
是类的一个方法。通过 new
关键字可以实例化一个类,并调用类的方法。
注意:类声明和函数声明之间的一个重要区别在于,函数声明会提升,类声明不会。在类中我们首先需要声明类,然后再访问它,否则代码将抛出ReferenceError
总结两种类的写法
使用原型的写法有一个缺点,在TS里面不好写,所以用TS的话尽量使用class的写法
方法一:使用原型 (ES5没有class如何实现类)
function Person(name,age)
{
this.name = name;
this.age = age;
}
//可共享的放到prototype上面
Person.prototype.sing = function(){
console.log("我会唱歌");
}
var p1 = new Person(ldh,18);
var p2 = new Person(fy,5);
方法二:使用class (ES6有了class之后)
// class 没有提供添加非函数属性的方法 prototype
class Person
{
//本身的属性写到constructor上面
constructor(name,age){
this.name = name;
this.age = age;
}
//共有的方法写到外面
sing(){
console.log("我会唱歌");
}
}
var p3 = new Person(zzz,18);
js的new做了什么
这个地方new Person('John', 25)
做了什么呢?
- 先创建了一个临时空对象 {}
- 给临时对象绑定原型Person
- 指定this = 临时对象
- 执行构造函数
- 返回临时对象,用
const person
接收到
类体static,private,protect,public有什么区别
在 JavaScript 中,类体内方法前面的 static
、private
、protected
和 public
是访问修饰符,用于定义类的成员的可访问性和作用域。
1、static
static
:静态方法属于类本身,而不是类的实例。可以直接通过类名调用,无需实例化对象。静态方法不能访问实例的属性和方法,只能访问静态属性和调用其他静态方法。
class MyClass {
static staticMethod() {
console.log('This is a static method.');
}
}
MyClass.staticMethod(); // 类调用静态方法,不能通过实例对象调用
2、private
private
:私有方法只能在类的内部访问,外部无法访问。可以使用 #
符号声明私有方法。
class MyClass {
#privateMethod() {
console.log('This is a private method.');
}
}
3、protected
protected
:受保护方法只能在类的内部和子类中访问,外部无法访问。使用 protected
关键字定义受保护方法。
所以protected比private多的就是子类中可以访问
class MyBaseClass {
protected protectedMethod() {
console.log('This is a protected method.');
}
}
class MySubClass extends MyBaseClass {
someMethod() {
this.protectedMethod(); // 可以访问父类的受保护方法
}
}
4、public
public
:公共方法是默认的访问修饰符,可以在类的内部和外部访问。
class MyClass {
publicMethod() {
console.log('This is a public method.');
}
}
const myObj = new MyClass();
myObj.publicMethod(); // 调用公共方法
继承
ES6之前的继承——组合继承
ES6之前并没有提供继承extents 我们可以通过借用父构造函数+原型对象来模拟实现继承,被称为组合继承
继承属性用call()
子构造函数中用call()
调用父构造函数以实现对父构造函数中属性的继承
1 实现函数的调用 可以等于 fn() = fn.call()
2 可以改变一个函数的指向 后面的传参就任意传,o只是用来改变指向
3 call的第一个参数this如果没有说明那就是全局对象window,在严格模式下是undefined
子构造函数继承父构造函数中的两个属性 核心就是,在子构造函数中调用父构造函数,并使用call把父构造函数的this指向子构造函数的this
//父构造函数
function Product(name, price) {
this.name = name;
this.price = price;
}
//子构造函数
function Food(name, price) {
Product.call(this, name, price); //子构造函数中调用父构造函数,并使用call把父构造函数的this指向子构造函数的this
this.category = 'food';
}
function Toy(name, price) {
Product.call(this, name, price);
this.category = 'toy';
}
var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);
继承方法
继承属性使用call(),那如何继承方法呢?
我们可能首先想到,直接把放公共函数prototype赋值,比如son.prototype = father.prototype
但这样的做法是不对的,这样相当于引用,两个都指向了同一块儿堆区内存
在子上面添加的方法也会到父上面去
// son.prototype.__proto__ = father.prototype //这句话被ban了
son.prototype = new Father(); //new Father会产生一个father的实例对象temp
//new会做四件事情
1 创建临时对象
2 this = 临时对象
3 this.__proto__ = 构造函数的 prototype
4 执行构造函数
5 return this
//而这个temp 是 === Father.prototype的 原型链
console.log(Son.prototype.constructor) //指向了Father
但是如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
// son.prototype = father.prototype
son.prototype = new Father(); //new Father会产生一个father的实例对象temp{}
//而这个temp 是 === Father.prototype的 原型链
son.prototype.constructor = Son;
ES6之后的继承——extent
ES6之后继承是通过使用 extends
关键字来实现的。子类可以继承父类的属性和方法,并可以扩展或重写它们。
以下是使用继承创建子类的示例:
class Student extends Person {
constructor(name, age, major) {
super(name, age); // 调用父类的构造函数
this.major = major;
}
introduce() {
console.log(`I'm studying ${this.major}.`);
}
}
const student = new Student('Alice', 20, 'Computer Science');
student.sayHello(); // 输出: Hello, my name is Alice and I'm 20 years old.
student.introduce(); // 输出: I'm studying Computer Science.
在上面的示例中,我们定义了一个名为 Student
的子类,它继承了 Person
父类。子类的构造函数中使用 super
关键字来调用父类的构造函数,并可以添加自己的属性和方法