一、面向过程和面向对象
面向过程(Procedure-Oriented Programming,简称POP)是一种编程模型,由一系列要执行的计算步骤组成,通常采用自上而下、顺序执行的方式。
面向对象编程(Object-oriented programming,简写:OOP)是一种计算机编程模型,它围绕数据或对象而不是功能和逻辑来组织软件设计,更专注于对象与对象之间的交互,对象涉及的方法和属性都在对象内部。说的更底层一点就是面向对象是一种依赖于类和对象概念的编程方式。
面向对象的三个基本特征:封装、继承、多态
- 封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 继承:通过继承创建的新类称为“子类”或“派生类”。继承的过程,就是从一般到特殊的过程。
- 多态:对象的多功能,多方法,一个方法多种表现形式
整章的重点
new 关键字,和 Object.create 方法,所构造出来的,对象关系。
const a = new A(); --> a.__proto__ = A.prototype;
{
a = Object.create(A.prototype);
}
const b = Object.create(c); --> b.__proto__ = c;
{
b.__proto__ = function.prototype = c;
function f(){};
f.prototype = c;
return new f();
}
二、构造函数、原型和原型链
构造函数
构造函数和普通函数本质上没什么区别,只不过使用了new关键字创建对象的函数,被叫做了构造函数。构造函数的首字母一般是大写,用以区分普通函数,当然不大写也不会有什么错误。
function Person(name,age){//Person是一个构造函数
/*构造函数中,实例成员就是构造函数内部通过this添加的成员,name、age、say就是实例成员(个人理解就是构造函数在实例化以后可以访问的成员)*/
this.name=name;
this.age=age;
this.say=function(){
console.log('我是人')
}
}
Person.height='165';//在构造函数上添加的成员就成为静态成员
var p1=new Person('张三',25);//p1-实例化对象
/*通过prototype添加的成员不是静态成员,是实例成员,也就是只要是实例化的对象都可以访问到*/
Person.prototype.weight='70kg';
console.log(p1.weight);//70kg
console.log(Person.weight);//undefined
/*静态成员只能通过构造函数进行访问*/
console.log(Person.height);//输出165
console.log(p1.height);//输出undefined
/*实例成员只能通过实例对象进行访问*/
console.log(p1.name);//输出张三
p1.say();//输出我是人
console.log(Person.age);//输出undefined
Person.say();//报错,Person.say is not a function
// 注意问题:
1.构造函数最好首字母大写,但是小写也不会影响程序执行
2.通过prototype为构造函数添加或者修改的属性和方法,访问到哪个内容主要是看访问的位置是在属性和方法添加之前还是之后,和实例化对象的位置没有关系。
构造函数注意事项
- 创造的Car可以称之为构造函数,也可以称之为类,构造函数就是类。
- c1,c2均为Car构造函数的实例对象。
- Car构造函数中的this指向Car的 实例对象即new Car()出来的对象
- 创建实例对象时必须带new
- 构造函数首字母大写,这是规范,请遵守它。如:Number() Array()
- constructor:这是实例对象都自动含有的属性,指向他们的构造函数
console.log(c1.constructor === Car)//true
- 每定义一个函数,这个函数就带有一个prototype的属性,__proto__指向被实例化的构造函数的prototype,prototype默认带有constructor属性,constructor指向构造函数。
原型、原型链
三、js 对象的创建
创建一个对象的几种方式
- Object.create( )
- 字面量创建 : var bziar = { }
const foo = Object.create({});
const bar = {}
// 为什么foo打印出来的原型链比bar多了一层?
foo.__proto__ // 相当于 {}
bar.__proto__ = Object.prototype; // bar的原型等于Object.prototype
foo.__proto__
foo.__proto__.__proto__ == Object.prototype // true
// 创建了一个对象
const q = {}
let p = Object.create(q) --> p.__proto__ = q
// p的原型,指向q:
当我们需要调用p对象的一个方法或者属性的时候,如果p上面没有,我会去q上面找
// 扩展
// 里面也只有一层,和bar一样
const baz= Object.create(Object.prototype);
const c = Object.create(null); // 什么也没有,里面么有任何方法
- new 关键字
function person(name) {
this.name = name
}
person.prototype.getName = function () {
console.log(this.name)
}
let p = new person('caoji')
// p对象的构造函数,是person
// 1. new 创建了一个对象,这个对象,指向了构造函数的原型。
p.__proto__ === Person.prototype;
// 2. 构造函数上,有个 原型 prototype , 原型里面,有个 constructor 函数,就是 构造函数自己。
Person.prototype.constructor === Person;
// 3. p 的构造函数,是 Person
p.constructor === Person;
new关键字,到底干了什么
- 首先创建一个空对象 let obj = { }
- 该对象的原型,指向了这个Function 的prototype
- 该对象实现了这个构造函数的方法
- 根据一些特定的情况,返回对象
- 如果没有返回值,则返回我创建的这个对象
- 如果有返回值,是一个对象,则返回该对象
- 如果有返回值,不是一个对象,则返回我创建的这个对象
// 第一种写法
function newFn(father) {
if (typeof father !== 'function') {
throw new Error('error');
}
// 1.把新对象的原型指针指向构造函数的原型属性
var obj = Object.create(father.prototype);
// 2.改变this指向,并且执行构造函数内部的代码(传参)
var result = father.apply(obj, Array.prototype.slice.call(arguments, 1));
// 3.判断函数执行结果的类型
return result && resule !== 'object' && result !== null ? result : obj;
}
const p = newFn(person, 123);
console.log('p===', p);
// 第二种写法
function One(name, age) {
this.name = name;
this.age = age;
}
// let a = new One();
// console.log(a);
//Fun为构造函数, args表示传参
function myNew(Fun, ...args) {
// 1.在内存中创建一个新对象
let obj = {};
// 2.把新对象的原型指针指向构造函数的原型属性
obj.__proto__ = Fun.prototype;
// 3.改变this指向,并且执行构造函数内部的代码(传参)
let res = Fun.apply(obj, args);
// 4.判断函数执行结果的类型
if (res instanceof Object) {
return res;
} else {
return obj;
}
}
let obj = myNew(One, 'XiaoMing', '18');
console.log('newObj:', obj);
四、继承
其实实现一个继承,主要就两个部分:
- 使用父类的构造函数方法和原型函数
- 让对象的原型链指向父类
ES5 原型链继承 - 构造函数继承 - 组合继承 - 组合寄生继承
ES6 class 继承
原型链继承
让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性。
function Parent() {
this.isShow = true
this.info = {
name: "mjy",
age: 18,
};
}
Parent.prototype.getInfo = function() {
console.log(this.info);
console.log(this.isShow);
}
function Child() {};
Child.prototype = new Parent();
let Child1 = new Child();
Child1.info.gender = "男";
Child1.getInfo(); // {name: 'mjy', age: 18, gender: '男'} ture
let child2 = new Child();
child2.isShow = false
console.log(child2.info.gender) // 男
child2.getInfo(); // {name: 'mjy', age: 18, gender: '男'} false
// 优点:写法方便简洁,容易理解。
// 隐含的问题
// 1. 如果有属性是引用的属性,一旦某个实例修改了这个属性,那么都会被修改。
// 2. 创建的 child 的时候,是不能传参数的,因为这个对象是一次性创建的(没办法定制化)。
构造函数的继承
在子类型构造函数的内部调用父类型构造函数;
使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。
function Parent(gender) {
this.info = {
name: "yhd",
age: 19,
gender: gender
}
}
function Child(gender) {
Parent.call(this, gender)
}
let child1 = new Child('男');
child1.info.nickname = 'xiaoma'
console.log(child1.info); // {name: 'yhd', age: 19, gender: '男', nickname: 'xiaoma'}
let child2 = new Child('女');
console.log(child2.info); //{name: 'yhd', age: 19, gender: '女'}
//优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。
// 1. 只能在构造函数中调用方法 函数不能复用
// 2. 不能继承原型属性/方法,只能继承父类的实例属性和方法
组合继承
将 原型链 和 借用构造函数 的组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性
function Parent (actions, name) {
this.actions = actions;
this.name = name
}
Parent.prototype.getName = function() {
console.log(this.name);
}
function Child(id) {
Parent.apply(this, Array.prototype.slice.call(arguments, 1));
this.id = id;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 优点:就是解决了原型链继承和借用构造函数继承造成的影响。
// 缺点:是无论在什么情况下,都会调用两次父类构造函数:
// 一次是在创建子类型原型的时候,另一次是在子类型构造函数内部
组合寄生式继承
最常用的继承方式,也是最佳的,组合继承会调用两次父类构造函数,存在效率问题。其实本质上子类原型最终是要包含父类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
//核心代码
function objectCopy(obj) {
function Fun() {}
Fun.prototype = obj;
return new Fun();
}
function inheritPrototype(child, parent) {
//生成一个父类原型的副本
let prototype = objectCopy(parent.prototype);
//重写这个实例的constructor
prototype.constructor = child;
//将这个对象副本赋值给 子类的原型
Child.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.hoby = ['唱', '跳'];
}
Parent.prototype.showName = function () {
console.log('my name is:', this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
//调用inheritPrototype函数给子类原型赋值,修复了组合继承的问题
inheritPrototype(Child, Parent);
Child.prototype.showAge = function () {
console.log('my age is:', this.age);
};
let child1 = new Child('mjy', 18);
child1.showAge(); // 18
child1.showName(); // mjy
child1.hoby.push('rap');
console.log(child1.hoby); // ['唱', '跳', 'rap']
let child2 = new Child('yl', 18);
child2.showAge(); // 18
child2.showName(); // yl
console.log(child2.hoby); // ['唱', '跳']
// 优点:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。
// 与此同时,原型链还能保持不变;
// 缺点:代码复杂
ES6、Class实现继承
原理ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。 ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this
优点:语法简单易懂,操作更方便。
缺点:并不是所有的浏览器都支持class关键字 lass Per
//父类Person
class Person{
constructor(name,sex){
this.name = name;
this.sex = sex;
this.say = function(){
console.log('say');
};
}
speak(){console.log("speak")}
static speak(){
console.log("static speak")
}
}
Person.version = '1.0';
//一:子类继承父类——语法:class 子类 extends 父类
class Programmer extends Person{
//在子类的构造方法中调用父类的构造方法
constructor(name,sex,feature){
// this.feature = feature; 三:×错误写法,super前面不能有this操作
super(name,sex);
this.feature = feature;
}
//二:同名覆盖:子类中声明的方法名和父类中的方法名相同时,子类中的方法将覆盖继承于父类的方法,采用自己的。
speak(){
console.log("Programmer speak");
}
}
const zs = new Programmer("张三","male","很高");
console.log(zs.name,zs.sex,zs.feature); //张三 male 很高
zs.say(); //say
//子类使用了自己的speak方法:
zs.speak();//Programmer speak
Programmer.speak();//static speak
//super作为函数调用
//代表父类的构造方法,只能用在子类的构造函数中,用在其他地方就会报错;
//super虽然代表了父类的构造方法,但是内部的this指向调用这个函数的类的实例
组合寄生继承 和 class 继承有什么区别?
- class 继承,会继承静态属性
- 子类中,必须在 constructor 调用 super, 因为子类自己的this 对象,必须先通过父类的构造函数完成。
名词解释
function Person(){} // 构造函数
let child = new Person() // child称之为Person构造函数的实例对象。
__proto__ 原型
object.prototype 原型对象
child 实例
function Person(){}
参考
- how-javascript-works 书籍:https://github.com/Troland/how-javascript-works
- 构造函数理解:blog.csdn.net/weixin_4911…
- 原型链经典图:dev-vroom-1311485584.cos.ap-beijing.myqcloud.com/js/20210112…
- 原型原型链理解:blog.csdn.net/weixin_5650…
- class继承详细介绍 blog.csdn.net/visiblefore…
- 继承 : juejin.cn/post/684490…