前言
本人在学习了JavaScript的核心内容后,为了整理自己对这些内容的理解,方便自己记忆和巩固特此写下博客来记录学习过程。
(一)对象
这里我们直接开始讲解创建对象,至于对象的一些自带属性请自行了解(如果后面有空我会考虑写一下)。
创建对象
虽然用对象字面量确实可以很方便的创建对象,但是可以发现创建具有同样接口的对象则需要写很多重复的代码。
1. 工厂模式
一种广泛应用于软件设计领域的设计模式。
//工厂模式
function createSoldier(name,age) {
let obj = new Object();
obj.name = name;
obj.age = age;
obj.kill = function () {
console.log(this.name+' 用枪射击');
}
return obj;
}
let s1 = createSoldier('jxm', 18);
let s2 = createSoldier('jza', 20);
s1.kill();
s2.kill();
虽然这种模式解决了重复代码的问题,但是并没有解决对象标识的问题,就是新创建的对象是什么类型
2. 构造函数模式
除了Object和Array这种自带的原生构造函数,我们还可以自定义构造函数(注意首字母大写)。
//构造函数模式
function Soldier(name,age) {
this.name = name;
this.age = age;
this.kill = function () {
console.log(`${this.name} 用枪射击了`);
}
}
let s1 = new Soldier('jxm', 20)
let s2 = new Soldier('mpc', 30)
s1.kill();
s2.kill();
这种方式得到的结果和工厂方法一样,区别在于 1、显示创建对象 2、属性和方法给了this 3、没有return 对象。这些都是new操作符给我们完成了如下操作
以上是面试常问的东西,也可以加深你对JavaScript对象的理解,之前说工厂模式无法给对象标识,
下面我们通过instanceof确定对象是哪个构造函数的实例。
补充说明
(1)任何函数只要用了new操作符调用就是构造函数(箭头函数除外,自己想想为什么)。
(2)调用一个函数在没有明确指定this的时候this指向Global对象,在浏览器中是window对象
构造函数的问题
当我们将之前的s1和s2变量console.dir打印出来发现
每个实例都有kill方法,这不是浪费嘛,所以这个模式的缺陷就需要原型模式来解决
3. 原型模式
只要创建一个函数,就会给这个函数一个prototype属性,它指向原型对象,所有的原型对象自动获得一个constructor指向与之关联的构造函数,如下图所示:
如果将一些属性和方法放到原型上,则实例可以共享这些属性与方法,
//原型模式
function Soldier() {}
Soldier.prototype.name = 'jxm';
Soldier.prototype.age = 18;
Soldier.prototype.kill = function () {
console.log(`${this.name} 用枪射击了`);
}
let s1 = new Soldier();
let s2 = new Soldier();
s1.kill();
s2.kill();
console.log(s1.kill == s2.kill);
可以看到这种方式就弥补了构造函数的缺点,共享了一些属性和方法。
每次调用构造函数去创建一个实例时,这个实例内部的[[Prototype]] (其实就是__proto__)指针就会指向构造函数的原型对象(prototype的内容),下图表示了Soldier构造函数和Soldier.prototype以及实例的关系
补充说明
(1)虽然可以通过实例来去读原型上的值,但是不能重写这些值,如果你给实例声明了和原型同名的属性,只会在实例上增加属性。
(2)即使通过将实例属性设置为null,也不会恢复它和原型之间的联系,除非用delete操作符,删除该属性,则可以让其可以在原型对象上搜索。
原型模式的问题
(1)弱化了向构造函数传初始化参数的能力,导致所有实例默认取得相同的属性值
(2)原型最大问题在于共享特性,如果prototype.friends属性是一个数组呢,那不就每个实例都共享同一个数组了。
(二)继承
这里我们只讨论常用的继承方式,常用的继承方式都是经过检验,且规避了一些问题比较好的继承方式(当然我哪天想通了想总结一下所有的继承方式也不是不行)
1. 原型链继承
//原型链继承
function Person(name) {
this.name = name;
this.colors =['red','green','blue']
}
Person.prototype.getName = function () {
console.log(`My name is ${this.name}`);
}
function Soldier(id) {
this.id = id;
}
//继承方法
Soldier.prototype = new Person();
Soldier.prototype.getInformation = function () {
console.log(`My name is ${this.name},and My id is ${this.id}`);
}
let p1 = new Soldier(1);
let p2 = new Soldier(2);
p1.colors.push('pink');
console.dir(p1);
console.dir(p2);
我们声明了一个Person构造函数,给其定义了两个属性name和colors,一个原型方法getName也声明了一个叫Soldier的构造函数,有id属性和原型方法getInformation,最后通过Soldier.prototype = new Person();实现Person的实例是Soldier的原型,下图是两个构造函数以及对应原型之间的关系
以下的输出结果是代码在浏览器控制台的输出结果
原型链继承的问题
1、原型中包含引用值就会在实例间共享
2、子类型在实例化时不能给父类型的构造函数传参
2. 盗用构造函数
//盗用构造函数
function Person(name) {
this.name = name;
this.colors = ['red', 'green', 'blue']
this.sayName = function () {
console.log(`My name is ${this.name}`);
}
}
function Soldier(id,name) {
//继承属性
Person.call(this,name);
this.id = id;
}
let p1 = new Soldier(1,'jxm');
let p2 = new Soldier(2,'mpc');
p1.colors.push('pink');
console.dir(p1);
console.dir(p2);
p1.sayName();
以上代码,通过在Soldier子类构造函数中调用父类构造函数属性私有化,每个实例都有自己的属性,解决了原型链继承方式含有引用值共用得问题;下图是代码得执行结果
盗用构造函数的问题
1、必须在构造函数中定义方法,导致重复定义函数,(我们在前面创建对象构造函数模式已经提到了这个问题)
2、子类型不能访问父类原型上定义得方法。
3. 组合继承(重点)
//组合继承
function Person(name, age) {
this.name = name;
this.age = age;
this.colors =['red','green','blue']
}
Person.prototype.getName = function () {
console.log(`My name is ${this.name}`);
}
Person.prototype.getAge = function () {
console.log(`My age is ${this.age}`);
}
function Soldier(name, age, id) {
//继承属性
Person.call(this,name,age);
this.id = id;
}
//继承方法
Soldier.prototype = new Person();
Soldier.prototype.getInformation = function () {
console.log(`My name is ${this.name},and I am ${this.age} years old ,My id is ${this.id}`);
}
let s1 = new Soldier('jxm', 18, 1);
s1.colors.push('pink');
console.log(s1.colors);
s1.getInformation()
s1.getAge()
let s2 = new Soldier('mpc', 30, 2)
console.log(s2.colors);
上面代码我们声明了一个Person的构造函数,定义了两个属性为name和age,同时在原型上定义了getName和getAge两个方法;接着又声明了Soldier构造函数,调用了Person构造函数并传入对应的参数,也定义了自己的属性id,通过Soldier.prototype = new Person()实现了Soldier构造函数的原型是Person的实例,原型链串起来了,接下来定义Soldier自己的原型方法getInformation
最后输出结果如下:
我们可以看到,我们实现了属性每个实例一份,s1和s2都有自己的colors数组,同时实例除了可以调用自己的原型方法,还可以通过原型链调用上层的原型方法,这种继承方式弥补了原型链和盗用构造函数继承方式得不足,是使用最多得继承模式。
4. 原型式继承
//原型式继承
function Object(obj) {
function F() { }
F.prototype = obj;
return new F();
}
let person = {
name: 'jxm',
friends:['Bob','Jhon','mpc']
}
let p1 = Object(person);
p1.friends.push('jza');
p1.name = 'Jason';
let p2 = Object(person)
p2.name ='zhang'
console.dir(p1)
console.dir(p2)
我们通过创建一个Object函数,里面创建了一个临时的构造函数,并且将传入的对象作为这个构造函数的原型对象,最后返回一个构造函数创建的实例,通过这个我们都可以猜测,由这个Object函数创建的实例,必然会共享属性,这跟我们原型模式有些类似;以下图片是代码的运行结果,可以看到我们的猜测完全没有问题,在ES5中我们使用Object.create( )的方式规范化了;
原型式继承适合不需要创建构造函数的,但对象要共享信息的场合;
5. 寄生式继承
function createObject(original){
let clone = object(original); // 通过调用函数创建一个新对象,只要能返回新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
这种继承方式适合于关注对象不在乎类型和构造函数的场景,跟构造函数模式类似无法复用函数。
6. 寄生式组合继承(重点)
//寄生式组合继承
function Person(name) {
this.name = name;
this.colors =['red','green','blue']
}
Person.prototype.getName = function () {
console.log(`My name is ${this.name}`);
}
function Soldier(name, id) {
//继承属性
Person.call(this,name,);
this.id = id;
}
function inherit(child, parent) {
// 创建对象
let prototype = Object.create(parent.prototype);
// 增强对象
prototype.constructor = child;
// 指定对象
child.prototype = prototype;
}
inherit(Soldier,Person);
Soldier.prototype.getInformation = function () {
console.log(`My name is ${this.name},and My id is ${this.id}`);
}
let s1 = new Soldier('jxm', 1);
s1.colors.push('pink');
console.log(s1.colors);
s1.getInformation()
let s2 = new Soldier('mpc', 2)
console.log(s2.colors);
基本思路: 不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已
我们使用组合继承的时候,由于调用了两次父类构造函数,这使得父类的实例(也是子类的原型)上有属性,同时由于子类实例化的时候也调用了父类构造函数,子类实例也有属性,子类跟父类实例上一样的属性,这种继承方式算是引用类型继承的最佳方式。
(三)类
前面的几种继承方式都各有各的问题,并且实现继承的代码比较冗长复杂,因此ES6引入了class来实现继承,这个类似于语法糖的东西本质上也是原型和构造函数的概念。
1. 类定义
class Person{}:这是类声明得方式
let Person = class {}:这是类表达式的方式
1、函数声明可以提升,但是类不能。
2、函数受到函数作用域限制,而类受块作用域限制
类可以包含构造函数方法、实例方法,获取函数、设置函数和静态方法,空类也有效,默认类定义的代码都在严格模式下执行
(1)空类的定义,有效 class Person {}
(2)有构造函数的类,有效 class Person { constructor() {} }
(3)有获取函数的类,有效 class Person { get myPerson() {} }
(4)有静态方法的类,有效 class Person { static myPerson() {} }
2. 类构造函数
constructor用于在类内部创建类的构造函数,它告诉了解释器使用new操作符创建类的实例时调用这个函数,如果类没有定义这个函数,则默认为空函数。
3. 实例、原型、类成员
//类的实例、原型、类成员
class Person{
constructor(name) {
this.name = name;
this.friends = ['jza','mpc'];
}
sayName() {
console.log(`My name is ${this.name}`);
}
get namex() {
console.log('get name');
}
}
let p = new Person('jxm');
p.namex
Person.greet = '你好';
Person.prototype.age = 18;
console.dir(Person)
以下图片是代码的运行结果:可以看到constructor里面的属性,会出现在实例上,在类块中定义的内容会出现在原型上,其他类成员可以在类外部定义
4. 类继承
(1)关键字extends
使用extends可以是继承一个类,也可以继承一个构造函数
//类继承
class Person {
constructor(name) {
this.name = name;
}
}
function Func() {
console.log('构造函数!')
}
class Soldier extends Person {
constructor(id,name) {
super(name);
this.id = id;
}
}
let s1 = new Soldier(1,'jxmjxm')
class log extends Func{}
let l = new log()
console.log(s1);
console.log(l instanceof Func);
以下图片是代码运行结果:
(2)关键字super
1、关键字super只能在派生类构造函数中和静态方法中使用,不能单独使用
2、调用super()会调用父类的构造函数,并且将返回的实例赋值给this
3、如果需要给父类构造函数传参,需要手动传入,在子类构造函数中不能在super之前使用this
4、如果子类显示定义了constructor构造函数,,要么在其中调用super(),要么在其中返回一个对象