在了解"原型"之前,你需要知道"面向对象"、"构造函数"等相关知识。
面向对象
- 面向对象实际是一种编程方式、思想。
- 提到面向对象,不得不提的是面向过程。
- 区别:
- 面向过程,关注的是每一步如何实现。
- 面向对象,只需要关注对象能做什么事。
- 举个例子:
- 比如你想吃冰淇淋
- 面向对象:你直接去超市或者商店,找到冰淇淋,付钱,开吃。
- 面向过程:你要准备冰淇淋的材料,还要知道制作的过程,制作好后,才能吃。
你可能还并没有完全了解面向对象的概念,但是没关系。再举个例子:
- 比如你想吃拉面,你可以选择自己制作,也可选择去面馆。
- 自己做就是“面向过程”,因为你需要买面,和面,拉面,煮面···最后吃面。
- 去面馆就是“面向对象”,因为你只需要和老板说,我要吃“牛肉拉面”,等一会就能吃面了。
你也可以去阿里云开发者社区查看大佬的解释,或自行百度。
对象的创建方式
内置构造函数
var obj = new Object(); // 创建一个对象
obj.year = 2022; // 为对象添加一个属性
// 为对象添加一个方法
obj.hiFn = function() {
console.log("你好,2022!")
}
字面量
// 定义一个对象,并赋初始值
var obj1 = {
name: 'tim',
age: 22
}
工厂函数
// 先定义一个函数
function factory() {
// 手动创建一个对象
var obj = new Object();
// 手动添加属性
obj.name = 'tim';
obj.age = 22;
// 手动返回这个对象
return obj;
}
// 外部再使用
var o1 = factory();
var o2 = factory();
自定义构造函数
// 先定义一个函数
function Person(name, age) {
// 自动创建一个对象,并赋值给this
// this = new Object();
// 手动添加属性
this.name = name;
this.age = age;
// 自动返回这个对象
// return this;
}
// 外部使用时,需要与new连用
var p1 = new Person('tim', 22);
console.log(p1); // { name: tim, age: 22}
构造函数知识点
- 构造函数与普通函数类似,之不过调用时需要与
new
连用 - 首字母用大驼峰命名,如
Person
、Animal
··· - 与
new
连用时,内部会自动创建一个对象,并返回对象,不需要写return
- 构造函数内部的
this
,指向当前实例。函数里this
的指向,由调用方式决定:- 作为声明式函数调用:
this ⇒ window
- 作为事件函数调用:
this ⇒ 事件源
- 作为对象的方法调用:
this ⇒ 调用方法的实例对象
- ···
- 作为声明式函数调用:
- 书写构造函数时,除了添加属性也可以添加方法
- 每new一次实例,方法都会单独开辟一块空间,即使是同一个方法
- 因此这会造成同一个方法占用多个内存空间,所以引入了
原型 prototype
原型
prototype
- 原型的出现,就是为了解决构造函数的缺点:同一个方法占用多个内存空间
- 每一个函数都自带一个属性,叫
prototype
,是一个对象空间,构造函数也有 - 可以通过函数名访问,也可以向该空间添加属性、方法,如:
Person.prototype
、Person.prototype.name = "tim"
- 在
prototype
虽然是通过函数添加的内容,但内容却是给实例使用的
// 定义一个函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在函数的原型上添加一个方法
Person.prototype.sleep = function() {
console.log("人都需要睡觉,7-8小时为宜。");
}
// 使用构造函数
var p1 = new Person("tim", 22);
console.log(p1.name); // tim
console.log(p1.age); // 22
console.log(p1.sleep); // 人都需要睡觉,7-8小时为宜。
proto
- 每一个对象都自带一个属性,叫
__proto__
,也是一个对象空间 - 实例对象也有
__proto__
,这个空间是指向该构造函数的prototype
- 所以,
__proto__
的地址与构造函数的prototype
的地址是同一个地址 - 当实例访问某个属性/方法时,会从自身查找。没找到会自动去
__proto__
里找 - 通常,在书写构造函数时:
- 属性写在构造函数的内部
- 方法写在构造函数的原型上
原型链
__proto__
里面有一个成员叫constructor
,是指向当前对象所属的构造函数- 如果,当一个对象不知道是谁构造的,就认为是由
Object
构造的 - 因为,
Object
是Js
中的顶级构造函数,有句话叫:“万物皆对象” - 所以,当找到
Object.prototype
时就到顶了,Object.prototype.__proto__
结果是null
- 由此形成了一个链状结构,就是"原型链"
- 原型链的规则:
- 访问规则
- 当访问一个对象的属性或方法时,会先从自身查找
- 如果没有找到,就会自动去
__proto__
里查找 - 如果还没找到,就会去
__proto__
的__proto__
里面查找 - 一直找到
Object.prototype
里面都没有找到,那么就会返回undefined
- 赋值规则
- 当需要给一个对象的属性或方法赋值时,会先从自身查找
- 如果找到了,就修改属性的值
- 如果没找到,就直接添加该属性并赋值
- 不会去
__proto__
里
- 访问规则
继承
- 继承指的是可以让子类使用父类的属性与方法
原型继承
- 通过改变原型链的方式达到继承
- 就是将子类的原型指向父类的实例,
Son.prototype = new Father();
- 缺点:
- 子类实例若修改了方法,会影响其他子类的方法,因为共用一个地址空间
// 父类
function Father(name){
this.name = name;
}
// 原型上有一个方法
Person.prototype.sayHi = function(){
console.log('hello, my son!')
}
// 2 子类
function Son(age){
this.age = age;
}
// 子类原型指向父类实例,就会拥有父类的属性与方法
Son.prototype = new Father('Jack');
// 使用子类,就可以使用的父类的属性与方法
var s1 = new Son(18);
console.log(s1);
构造函数继承
- 在子类的构造函数中,使用父类的构造函数,并修改
this
指向,指到子类实例 - 使用
call
来改变函数内部this
指向,call(子类实例,传递给函数的参数, ...)
- 优点:可以将继承自父类的属性直接写在自身
- 缺点:
- 只能继承父类函数体里的内容,不能继承父类原型上的内容
- 因为,函数体里通常写的是属性,而方法通常是写在原型上的
// 父类
function Father(name, gender){
this.name = name;
this.gender = gender;
}
Father.prototype.sayHi = function(){
console.log('hello, my son!');
}
// 子类继承父类
function Son(age, name, gender){
this.age = age; // 子类自身的属性
// 使用call将this指向当前实例,并传入父类的属性
Person.call(this, name, gender); // 继承父类的属性
}
// 子类的实例对象,就会拥有父类的属性
var s1 = new Son(18, "小红", '女');
组合继承
- 由于上述两种方法,都有各自的缺点,所以将两者结合,就是组合继承
- 先使用构造函数继承,将属性继承到自身
- 再使用原型继承,将方法继承到自身
- 缺点:
- 子类自身原型上的方法,会被覆盖
- 所以,必须要继承以后,再为子类添加原型方法
// 父类
function Father(name, gender){
this.name = name;
this.gender = gender;
}
Father.prototype.sayHi = function(){
console.log('hello, my son!');
}
// 子类
function Son(age, name, gender){
this.age = age;
// 先使用构造函数 继承属性
Father.call(this, name, gender);
}
// 再使用原型 继承方法
Son.prototype = new Father();
// 最后再添加 子类自身的方法
Son.prototype.sonFn = function(){
console.log("我是子类自身的方法")
}
// 最终可以使用父类与子类的属性与方法
var s2 = new Student(20, "小明", '男');
ES6的类语法
-
学习
ES6
的class
语法之前,先复习下之前使用构造函数如何定义属性与方法// 构造函数 function Human(name, age){ this.name = name; this.age = age; } // 在原型上添加方法 Human.prototype.eat = function(){ console.log("一顿不吃饿得慌") }
-
然后再来对比,
ES6
的类语法// 使用关键字 class 定义一个函数 class Human{ // 使用关键字 constructor 定义函数体内部的属性 constructor(name, age) { this.name = name; this.age = age; } // 直接书写函数 就是在原型上定义的方法 等同于 Human.prototype.eat eat(){ console.log("一顿不吃饿得慌") } // 使用关键字 static定义静态属性 该属性是给Human函数使用的 static year = 2022; static sayHi = function() { console.log("Hello world!"); }; } // 实例化 var h1 = new Human("小明", 20); // 使用非static定义的属性与方法 console.log(Human.year); // 2022 使用static定义的静态属性 console.log(Human.sayHi()); // 使用static定义的静态方法
ES6继承
- 可以使用
extends
关键字:class Son extends Father{ }
- 继承父类使用
super
关键字:super(name, age);
,并且必须先写
// 父类
class Father{
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log("Hello, my son!")
}
}
// 子类 使用extends关键字表示要继承父类
class Son extends Father{
constructor(gender, name, age) {
// 必须先写super,表示要继承父类的属性,并传入参数
super(name, age);
// 然后再书写自身的属性
this.gender = gender;
}
// 子类自身的方法
sayHello() {
console.log("Hello, my father!")
}
}
// 实例就会有父类的属性与方法
var s1 = new Son("男", "李磊", 22)
console.log(s1);
- 子类通过
extends
表示要继承父类 - 然后通过
super
来继承父类的属性 - 父类的方法不需要写其他的,会通过原型链自动查找继承
【自己总结的,难免有错,仅供参考】