第三节 面向对象


代码,是前端工程师的“武器”,也是他们的“面包和黄油”。


如果要我总结一下学习前端以来我遇到了哪些瓶颈,那么面向对象一定是第一个会想到的。尽管现在对于面向对象有了一些的了解,但是当初那种似懂非懂的痛苦,依然历历在目。

为了帮助大家能够更加直观的学习和了解面向对象,我会用尽量简单易懂的描述来展示面向对象的相关知识。并且也准备了一些实用的例子帮助大家更加快速的掌握面向对象的真谛。

  • jQuery的面向对象实现
  • 封装拖拽
  • 简易版运动框架封装

一、对象的定义

在ECMAScript-262中,对象被定义为**“无序属性的集合,其属性可以包含基本值,对象或者函数”**。

也就是说,在JavaScript中,对象无非就是由一些列无序的key-value对组成。其中value可以是基本值,对象或者函数。

// 这里的person就是一个对象
var person = {
    name: 'Tom',
    age: 18,
    getName: function() {},
    parent: {}
}

创建对象

我们可以通过new的方式创建一个对象。

var obj = new Object();

也可以通过对象字面量的形式创建一个简单的对象。

var obj = {};

Object.create()

以一个现有对象作为原型,创建一个新对象。

Object.create(proto, [propertiesObject])
// proto: 新创建对象的原型对象
// propertiesObject: 可选参数。要添加新对象的可枚举属性(新添加的属性是自身的属性,而不是其原型链上的属性)

var obj = Object.create(null);
// 创建的对象是不继承Object原型链上的属性,如tostring()方法

在这里插入图片描述

底层实现

Object.create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};

参考:Object.create(null) 和{}的区别

当想要给我们创建的简单对象添加方法时,可以这样表示,

// 可以这样
var person = {};
person.name = "TOM";
person.getName = function() {
    return this.name;
}

// 也可以这样
var person = {
    name: "TOM",
    getName: function() {
        return this.name;
    }
}

访问对象的属性和方法

假如我们有一个简单的对象如下:

var person = {
    name: 'TOM',
    age: '20',
    getName: function() {
        return this.name
    }
}

当我们想要访问他的name属性时,可以用如下两种方式访问。

person.name

// 或者
person['name']

如果想要访问的属性名是一个变量时,常常会使用第二种方式。例如我们要同时访问person的name与age,可以这样写:

['name', 'age'].forEach(function(item) {
    console.log(person[item]);
})

这种方式一定要重视,记住它以后在我们处理复杂数据的时候会有很大的帮助。

属性的类型

属性分为两种:数据属性和访问器属性。

数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有4个特性描述它们的行为。

  • [[Configurable]]:表述属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象的属性的这个特性都是 true。
  • [[Enumerable]]:表述属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象的属性的这个特性都是为 true。
  • [[Writable]]:表述属性的值是否可以被修改。默认情况下,所有直接定义在对象的属性的这个特性都是为 true。
  • [[Value]]:包含属性实际的值。读取和写入属性值的位置。这个特性的默认值为 undefined。

属性默认特性的修改

Object.defineProperty(obj, key, descriptor)

访问器属性

访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这个两个函数不是必需的。

在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。

  • [[Configurable]]:表述属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象的属性的这个特性都是 true。
  • [[Enumerable]]:表述属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象的属性的这个特性都是为 true。
  • [[Get]]:获取函数,在读取属性时调用。默认值为undefined。
  • [[Set]]:设置函数,在写入属性时调用。默认值为undefined。

访问器属性是不能直接定义的,必须使用 Object.defineProperty()

合并对象

方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举和自有属性赋值到目标对象。以字符串和符号为键的属性会被复制。

Object.assign(targetObject, sourceObject, ...)

Object.assign() 实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。

对象标识及相等判定

Object.is(Obj1, Obj2)

增强的对象语法

  • 属性值简写
  • 简写方法名

对象解构

使用与对象匹配的解构来实现对属性的赋值。

二、工厂模式

使用上面的方式创建对象很简单,但是在很多时候并不能满足我们的需求。就以person对象为例,假如在实际开发中,不仅仅需要一个名字叫做TOM的person对象,同时还需要另外一个名为Jake的person对象,虽然他们有很多相似之处,但是我们不得不重复写两次。

var perTom = {
    name: 'TOM',
    age: 20,
    getName: function() {
        return this.name
    }
};

var perJake = {
    name: 'Jake',
    age: 22,
    getName: function() {
        return this.name
    }
}

很显然这并不是合理的方式,当相似对象太多时,大家都会崩溃掉。

可以使用工厂模式解决这个问题。顾名思义,工厂模式就是我们提供一个模子,然后通过这个模子复制出需要的对象。我们需要多少个,就复制多少个。

var createPerson = function(name, age) {

    // 声明一个中间对象,该对象就是工厂模式的模子
    var o = new Object();

    // 依次添加我们需要的属性与方法
    o.name = name;
    o.age = age;
    o.getName = function() {
        return this.name;
    }

    return o;
}

// 创建两个实例
var perTom = createPerson('TOM', 20);
var PerJake = createPerson('Jake', 22);

工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)

var obj = {};
var foo = function() {}

console.log(obj instanceof Object);  // true
console.log(foo instanceof Function); // true

三、构造函数

构造函数是用于创建特定类型对象的。像 Object 和 Arrary 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己对象类型定义属性和方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        return this.name;
    }
}

let person1 = new Person("Greg", 17)

function demo() {
    console.log(this);
}

demo();  // window
new demo();  // demo

构造函数内部的代码和工厂函数基本是一致的,区别在于:

  • 没有显示地创建对象
  • 属性和方法直接赋值给了 this
  • 没有 return

new关键字

要创建对象实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作:

  1. 声明一个新对象;
  2. 将新对象的原型[[Prototype]]特性被赋值为构造函数的 prototype 属性;
  3. 构造函数内部的this 被赋值为新对象(即指向该新对象);
  4. 如果构造函数返回非空对象,则返回值该对象;否则,返回刚创建的新对象。
// 创建一个构造函数,其实该函数与普通函数并无区别
var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        return this.name;
    }
}

// 将构造函数以参数形式传入
function New(func) {

    // 0-声明一个中间对象,该对象为最终返回的实例
    var res = {};
    if (func.prototype !== null) {
        // 1-将实例的原型指向构造函数的原型
        res.__proto__ = func.prototype;
    }

    // ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));

    // 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret;
    }

    // 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
    return res;
}

// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res
var p1 = New(Person, 'tom', 20);
console.log(p1.getName());

// 当然,这里也可以判断出实例的类型了
console.log(p1 instanceof Person); // true

为了能够判断实例与对象的关系,我们就使用构造函数来搞定。

var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        return this.name;
    }
}

var p1 = new Person('Ness', 20);
console.log(p1.getName());  // Ness

console.log(p1 instanceof Person); // true

构造函数也是函数

构造函数与普通函数唯一区别就是调用方式不同。除此之外构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用 new 操作符调用就是构造函数,而不适用 new 操作符调用的函数就是普通函数。

构造函数的问题

构造函数的主要问题在于,其定义的方法会在每个实例上都创建创建一遍。要解决这个问题,可以把函数定义转移到构造函数外部。(此方法缺点,扰乱了全局作用域),更好的方式是定义在 原型 prototype 属性上。

无论何时,只要创建一个函数,就会按照特定规则为这个函数创建一个 prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指向与之关联的构造函数。

在自定义构造函数时,原型对象默认只会获取 constructor 属性,其他而定所有方法都继承自 Object。每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。

四、原型

虽然构造函数解决了判断实例类型的问题,但是,说到底,还是一个对象的复制过程。跟工厂模式颇有相似之处。也就是说,当我们声明了100个person对象,那么就有100个getName方法被重新生成。

这里的每一个getName方法实现的功能其实是一模一样的,但是由于分别属于不同的实例,就不得不一直不停的为getName分配空间。这就是工厂模式存在的第二个麻烦。

显然这是不合理的。我们期望的是,既然都是实现同一个功能,那么能不能就让每一个实例对象都访问同一个方法?

当然能,这就是原型对象要帮我们解决的问题了。

我们创建的每一个函数,都可以有一个prototype属性,该属性指向一个对象。这个对象,就是我们这里说的原型

当我们在创建对象时,可以根据自己的需求,选择性的将一些属性和方法通过prototype属性,挂载在原型对象上。而每一个new出来的实例,都有一个__proto__属性,该属性指向构造函数的原型对象,通过这个属性,让实例对象也能够访问原型对象上的方法。因此,当所有的实例都能够通过__proto__访问到原型对象时,原型对象的方法与属性就变成了共有方法与属性。

我们通过一个简单的例子与图示,来了解构造函数,实例与原型三者之间的关系。

由于每个函数都可以是构造函数,每个对象都可以是原型对象,因此如果在理解原型之初就想的太多太复杂的话,反而会阻碍你的理解,这里我们要学会先简化它们。就单纯的剖析这三者的关系。

// 声明构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 通过prototye属性,将方法挂载到原型对象上
Person.prototype.getName = function() {
    return this.name;
}

// 实例
var p1 = new Person('tim', 10);
var p2 = new Person('jak', 22);
console.log(p1.getName === p2.getName); // true

在这里插入图片描述
通过图示我们可以看出,构造函数的prototype与所有实例对象的__proto__都指向原型对象。而原型对象的constructor指向构造函数。

除此之外,还可以从图中看出,实例对象实际上是对前面我们所说的中间对象的复制,而中间对象中的属性与方法都在构造函数中添加。于是根据构造函数与原型的特性,我们就可以将在构造函数中,通过this声明的属性与方法称为私有变量与方法,它们被当前被某一个实例对象所独有。而通过原型声明的属性与方法,我们可以称之为共有属性与方法,它们可以被所有的实例对象访问。

当我们访问实例对象中的属性或者方法时,会优先访问实例对象自身的属性和方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        console.log('this is constructor.');
    }
}

Person.prototype.getName = function() {
    return this.name;
}

var p1 = new Person('tim', 10);

p1.getName(); // this is constructor.

在这个例子中,我们同时在原型与构造函数中都声明了一个getName函数,运行代码的结果表示原型中的访问并没有被访问。

我们还可以通过in来判断,一个对象是否拥有某一个属性/方法,无论是该属性/方法存在于实例对象还是原型对象。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.getName = function() {
    return this.name;
}

var p1 = new Person('tim', 10);

console.log('name' in p1); // true
 
in的这种特性最常用的场景之一,就是判断当前页面是否在移动端打开。
 
isMobile = 'ontouchstart' in document;

// 很多人喜欢用浏览器UA的方式来判断,但并不是很好的方式

更简单的原型写法

根据前面例子的写法,如果我们要在原型上添加更多的方法,可以这样写:

function Person() {}

Person.prototype.getName = function() {}
Person.prototype.getAge = function() {}
Person.prototype.sayHello = function() {}
... ...

除此之外,我还可以使用更为简单的写法。

function Person() {}

Person.prototype = {
    constructor: Person,
    getName: function() {},
    getAge: function() {},
    sayHello: function() {}
}

这种字面量的写法看上去简单很多,但是有一个需要特别注意的地方。Person.prototype = {}实际上是重新创建了一个{}对象并赋值给Person.prototype,这里的{}并不是最初的那个原型对象。因此它里面并不包含constructor属性。为了保证正确性,我们必须在新创建的{}对象中显示的设置constructor的指向。即上面的constructor: Person。

五、原型链

原型对象其实也是普通的对象。几乎所有的对象都可能是原型对象,也可能是实例对象,而且还可以同时是原型对象与实例对象。这样的一个对象,正是构成原型链的一个节点。因此理解了原型,那么原型链并不是一个多么复杂的概念。

我们知道所有的函数都有一个叫做toString的方法。那么这个方法到底是在哪里的呢?

先随意声明一个函数:

function add() {}

那么我们可以用如下的图来表示这个函数的原型链。
在这里插入图片描述
其中add是Function对象的实例。而Function的原型对象同时又是Object的实例。这样就构成了一条原型链。原型链的访问,其实跟作用域链有很大的相似之处,他们都是一次单向的查找过程。因此实例对象能够通过原型链,访问到处于原型链上对象的所有属性与方法。这也是foo最终能够访问到处于Object原型对象上的toString方法的原因。

基于原型链的特性,我们可以很轻松的实现继承。

六、继承

我们常常结合构造函数与原型来创建一个对象。因为构造函数与原型的不同特性,分别解决了我们不同的困扰。因此当我们想要实现继承时,就必须得根据构造函数与原型的不同而采取不同的策略。

我们声明一个Person对象,该对象将作为父级,而子级cPerson将要继承Person的所有属性与方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.getName = function() {
    return this.name;
}

首先我们来看构造函数的继承。在上面我们已经理解了构造函数的本质,它其实是在new内部实现的一个复制过程。而我们在继承时想要的,就是想父级构造函数中的操作在子级的构造函数中重现一遍即可。我们可以通过call方法来达到目的。

// 构造函数的继承
function cPerson(name, age, job) {
    Person.call(this, name, age);
    this.job = job;
}

原型的继承,只需要将子级的原型对象设置为父级的一个实例,加入到原型链中即可。

// 继承原型
cPerson.prototype = new Person(name, age);

// 添加更多方法
cPerson.prototype.getLive = function() {}

在这里插入图片描述
当然关于继承还有更好的方式。

七、更好的继承

假设原型链的终点Object.prototype为原型链的E(end)端,原型链的起点为S(start)端。

通过前面原型链的学习我们知道,处于S端的对象,可以通过S -> E的单向查找,访问到原型链上的所有方法与属性。因此这给继承提供了理论基础。我们只需要在S端添加新的对象,那么新对象就能够通过原型链访问到父级的方法与属性。因此想要实现继承,是一件非常简单的事情。

因为封装一个对象由构造函数与原型共同组成,因此继承也会分别有构造函数的继承与原型的继承。

假设我们已经封装好了一个父类对象Person。如下。

var Person = function(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.getName = function() {
    return this.name;
}

Person.prototype.getAge = function() {
    return this.age;
}
 

构造函数的继承比较简单,我们可以借助call/apply来实现。假设我们要通过继承封装一个Student的子类对象。那么构造函数可以如下实现。

var Student = function(name, age, grade) {
    // 通过call方法还原Person构造函数中的所有处理逻辑
   Person.call(this, name, age);
    this.grade = grade;
}

// 等价于
var Student = function(name, age, grade) {
    this.name = name;
    this.age = age;
    this.grade = grade;
}

原型的继承则稍微需要一点思考。首先我们应该考虑,如何将子类对象的原型加入到原型链中?我们只需要让子类对象的原型,成为父类对象的一个实例,然后通过__proto__就可以访问父类对象的原型。这样就继承了父类原型中的方法与属性了。

因此我们可以先封装一个方法,该方法根据父类对象的原型创建一个实例,该实例将会作为子类对象的原型。

function create(proto, options) {
    // 创建一个空对象
    var tmp = {};

    // 让这个新的空对象成为父类对象的实例
    tmp.__proto__ = proto;

    // 传入的方法都挂载到新对象上,新的对象将作为子类对象的原型
    Object.defineProperties(tmp, options);
    return tmp;
}

简单封装了create对象之后,我们就可以使用该方法来实现原型的继承了。

Student.prototype = create(Person.prototype, {
    // 不要忘了重新指定构造函数
    constructor: {
        value: Student
    }
    getGrade: {
        value: function() {
            return this.grade
        }
    }
})

那么我们来验证一下我们这里实现的继承是否正确。

var s1 = new Student('ming', 22, 5);

console.log(s1.getName());  // ming
console.log(s1.getAge());   // 22
console.log(s1.getGrade()); // 5

全部都能正常访问,没问题。在ECMAScript5中直接提供了一个Object.create方法来完成我们上面自己封装的create的功能。因此我们可以直接使用Object.create.

Student.prototype = create(Person.prototype, {
    // 不要忘了重新指定构造函数
    constructor: {
        value: Student
    }
    getGrade: {
        value: function() {
            return this.grade
        }
    }
})

完整代码如下:

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.getName = function() {
    return this.name
}
Person.prototype.getAge = function() {
    return this.age;
}

function Student(name, age, grade) {
    // 构造函数继承
    Person.call(this, name, age);
    this.grade = grade;
}

// 原型继承
Student.prototype = Object.create(Person.prototype, {
    // 不要忘了重新指定构造函数
    constructor: {
        value: Student
    }
    getGrade: {
        value: function() {
            return this.grade
        }
    }
})

var s1 = new Student('ming', 22, 5);

console.log(s1.getName());  // ming
console.log(s1.getAge());   // 22
console.log(s1.getGrade()); // 5

基本代码

首先定义一个父类:

// 构造函数
function Animal(name) {
	this.name = name || 'Animal';
	this.sleep = function() {
		console.log(this.name + '正在睡觉!');
	};
}
// 原型上面的方法:
Animal.prototype.eat = function(food) {
	console.log(this.name + '正在吃:' + food);
}

实现继承的方法

(1)原型链继承

// 将父类的实例作为子类的原型
function Dog() {}
// 将Animal的实例挂载到了Dog的原型链上
Dog.prototype = new Animal();
// 或:
// Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.name = 'dog';

var dog = new Dog();
console.log(dog.name);		// dog
dog.eat('bone');		// dog正在吃:bone
dog.sleep();		// dog正在睡觉!
console.log(dog instanceof Animal);		// true
console.log(dog instanceof Dog);		// true

特点:

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  • 父类新增原型方法/原型属性,子类都能访问的到
  • 简单

缺点:

  • 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  • 无法实现继承多个来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参

demo:

  function Parent1() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child1() {
    this.type = 'child2';
  }
  Child1.prototype = new Parent1();
  console.log(new Child1());

  let s1 = new Child1();
  let s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play);

(2)构造函数继承

使用父类的构造函数增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name) {
	Animal.call(this);
	this.name = name || 'Tom';
}

var cat = new Cat();
console.log(cat.name);		// Tom
cat.sleep();		// Tom正在睡觉!
console.log(cat instanceof Animal);		// false
console.log(cat instanceof Cat);		// true

特点:

  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点:

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

demo:

  function Parent1(){
    this.name = 'parent1';
  }
 
  Parent1.prototype.getName = function () {
    return this.name;
  }
 
  function Child1(){
    Parent1.call(this);
    this.type = 'child1'
  }
 
  let child = new Child1();
  console.log(child);  // 没问题
  console.log(child.getName());  // 会报错

(3)组合继承(构造函数+原型链)

通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

function Cat(name) {
	Animal.call(this);
	this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat = new Cat();
console.log(cat.name);	//Tom
cat.sleep();		//Tom正在睡觉
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

特点:

  • 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  • 既是子类的实例,也是父类的实例
  • 不存在引用属性共享问题
  • 函数可复用
  • 可传参

缺点:

  • 调用了两次构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

demo:

  function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }
 
  Parent3.prototype.getName = function () {
    return this.name;
  }
  function Child3() {
    // 第二次调用 Parent3()
    Parent3.call(this);
    this.type = 'child3';
  }
 
  // 第一次调用 Parent3()
  Child3.prototype = new Parent3();
  // 手动挂上构造器,指向自己的构造函数
  Child3.prototype.constructor = Child3;
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play);  // 不互相影响
  console.log(s3.getName()); // 正常输出'parent3'
  console.log(s4.getName()); // 正常输出'parent3'

(4)寄生组合式继承

通过寄生方式,砍掉父类的实例属性,这样,在调用俩次父类的构造的时候,就不会初始化俩次实例方法/属性,避免了组合继承的缺点。

function Cat(name) {
	Animal.call(this);
	this.name = name || 'Tom';
}
(function() {
	var Super = function() {};  // 创建一个没有实例的方法类
	Super.prototype = Animal.prototype;
	Cat.prototype = new Super();  // 将实例作为子类的原型
})();

let cat = new Cat();
console.log(cat.name);		// Tom
cat.sleep();		// Tom正在睡觉
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

Cat.prototype.constructor = Cat;	// 修复构造函数
  • 特点:基本上是完美的

  • 缺点:实现起来较为复杂

demo:

  function clone (parent, child) {
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
  }
 
  function Parent6() {
    this.name = 'parent6';
    this.play = [1, 2, 3];
  }
   Parent6.prototype.getName = function () {
    return this.name;
  }
  function Child6() {
    Parent6.call(this);
    this.friends = 'child5';
  }
 
  clone(Parent6, Child6);
 
  Child6.prototype.getFriends = function () {
    return this.friends;
  }
 
  let person6 = new Child6();
  console.log(person6);
  console.log(person6.getName());
  console.log(person6.getFriends());

(5)原型式继承

采用原型式继承并不需要定义一个类,传入参数obj,生成一个继承obj对象的对象。

let parent4 = {
  name: "parent4",
  friends: ["p1", "p2", "p3"],
  getName: function() {
    return this.name;
  }
};
 
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");

let person5 = Object.create(parent4);
person5.friends.push("lucy");
 
console.log(person4.name);
console.log(person4.name === person4.getName());
console.log(person5.name);
console.log(person4.friends);
console.log(person5.friends);

优点:采用原型式继承并不需要定义一个类,传入参数obj,生成一个继承obj对象的对象。当只想单纯地让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。
缺点:不是类式继承,而是原型式基础,缺少了类的概念。

(6)寄生式继承

创建一个仅仅用于封装继承过程的函数,然后在内部以某种方式增强对象,最后返回对象。

let parent5 = {
  name: "parent5",
  friends: ["p1", "p2", "p3"],
  getName: function() {
    return this.name;
  }
};
 
function clone(original) {
  let clone = Object.create(original);
  clone.getFriends = function() {
    return this.friends
  };
  return clone;
}
 
let person5 = clone(parent5);
console.log(person5.getName());
console.log(person5.getFriends());

优点:原型式继承的扩展(其实就是在原型式继承得到对象的基础上,在内部再以某种方式来增强对象,然后返回)
缺点:依旧没有类的概念

(7)class+extends继承

class Person {
  constructor(name) {
    this.name = name
  }
  // 原型方法
  // 即 Person.prototype.getName = function() { }
  // 下面可以简写为 getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}
 
class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    super(name)
    this.age = age
  }
}
 
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

demo2:

/** ES5 通过修改原型链实现继承 */
(function() {
  // 1. 创建两个构造函数
  function Animal(name, age) {
    this.name = name;
    this.age = age;
  }
  Animal.prototype.eat = function (type) {
    return `${type} food`;
  }

  function Human(name, age, work) {
    /**
     * apply: 调用一个对象的一个方法,用另一个对象替换当前对象。
     *		例如:B.apply(A, arguments);即A对象调用B对象的方法。
     * call:调用一个对象的一个方法,用另一个对象替换当前对象。
     * 		例如:B.call(A, args1,args2);即A对象调用B对象的方法。
     * 
     * 两者最大的取别就是: apply 传入一个 arguments, call 是一个一个的传参;
     */

     /**
      * bind & call: 两者的使用方法一致,
      *  取别: call 是立即执行,
      *        bind() 方法创建一个新的函数,在 bind() 被调用时,
      * 					这个新函数的 this 被指定为 bind() 的第一个参数,
      * 					而其余参数将作为新函数的参数,供调用时使用。
      */
    
    // 2. 在 子构造函数中 调用 父构造函数
    Animal.call(this, ...[name, age]);
    // Animal.apply(this, [name, age]);
    this.work = work;
  }

  /**
   * 3. 实现继承,将子类的原型对象 prototype 指向 父类 原型对象,
   */

  // 让 Human 这个构造函数的 原型对象 指向 animal 的实例,
  // 这样的,实例的 __proto__ 也会被 human 继承;
  // Human.prototype = new Animal()

  Human.prototype = Object.create(Animal.prototype, {
    constructor: {
      value: Human,
    }
  })

  Human.prototype.constructor = Human

  const codePerson = new Human('阿伟', 27, 'coder')
  // console.log(codePerson, codePerson.eat('cooked'));
  
})();

/** class 继承 */
(function() {
  class Animal {
    constructor(name, type) {
      this.name = name
      this.type = type
    }
    getName() {
      return this.name
    }
    static getType() {
      return 'all animal'
    }
  }

  /**
   * 1. class 通过 extends 关键字 实现继承
   */
  class Human extends Animal {
    // 
    constructor (name, type, work) {
/**
  * 2. 子类必须在 constructor 方法中调用 super 方法,此 super 方法表示 
父类的 constructor
  * 
  * 此时 super 相当于 Animal.call(this, ...[name, age]);
  * 
  * 注意: 
  * 在子类的构造函数中,只有调用super之后, 才可以使用this关键字,否则会报错;
  * 且super必须在 constructor 的第一行调用
*/
      super(name, type)
      this.work = work
    }

  }
  const codePerson = new Human('阿伟', '27', 'coder')
  console.log(codePerson.getName()) // 阿伟 

  // 3. Human 会继承 父类的静态方法
  console.log(Human.getType()) // all animal

  // 4. 可以通过 getprototypeOf() 判断 一个类 是否继承自 另一个类;
  console.log(Object.getPrototypeOf(Human) === Animal) // true
})();

(8)实例继承

为父类实例添加新特性,作为子类实例返回

function Cat(name) {
	var instance = new Animal();
	instance.name = name || 'Tom';
	return instance;
}

var cat = new Cat();
console.log(cat.name);		// Tom
cat.sleep();			// Tom正在睡觉!
console.log(cat instanceof Animal);		// true
console.log(cat instanceof Cat);		// false

特点:

  • 不限制调用方式,不管是new子类()还是子类(),返回的对象都具有相同的效果

缺点:

  • 实例是父类的实例,不是子类的实例
  • 不支持多继承

demo:

//动物类
function Animal(name, sex) {
  this.name = name || 'Animal';
  this.sex = sex || '未知';
  this.sleep = function () {
    console.log(this.name + "在睡觉!");
  }
}
//添加原型属性和方法
Animal.prototype = {
  eat: function () {
    console.log(this.name + "在吃饭!");
  },
  play: function () {
    console.log(this.name + "在玩!");
  }
}

//子类
function Mouse(){
  var animal=new Animal();
  return animal;
}
//下面这种写法可以使用
//var mouse=new Mouse();
var mouse=Mouse();
console.log(mouse);

//实例继承  子类的实例不是本身  是父类
console.log(mouse instanceof  Mouse);//false
console.log(mouse instanceof  Animal);//true

(9)拷贝继承

function Cat(name){
	var animal = new Animal();
	for(let i in animal) {
		Cat.prototype[i] = animal[i];
	}
	Cat.prototype.name = name || 'Tom';
}

var cat = new Cat();
console.log(cat.name);	//Tom
cat.sleep();	//Tom正在睡觉!
console.log(cat instanceof Animal); 	// false
console.log(cat instanceof Cat);	 // true

特点:

  • 支持多继承

缺点:

  • 效率极低,内存占用高(因为要拷贝父类的属性)
  • 无法获取父类不可枚举的方法(for in不能访问到的)
    在这里插入图片描述

八、属性类型

在上面的继承实现中,使用了一个大家可能不太熟悉的方法defineProperties。并且在定义getGrade时使用了一个很奇怪的方式。

getGrade: {
    value: function() {
        return this.grade
    }
}

这其实是对象中的属性类型。在我们平常的使用中,给对象添加一个属性时,直接使用object.param的方式就可以了,或者直接在对象中挂载。

var person = {
    name: 'TOM'
}

在ECMAScript5中,对每个属性都添加了几个属性类型,来描述这些属性的特点。他们分别是

  • configurable: 表示该属性是否能被delete删除。当其值为false时,其他的特性也不能被改变。默认值为true
  • enumerable: 是否能枚举。也就是是否能被for-in遍历。默认值为true
  • writable: 是否能修改值。默认为true
  • value: 该属性的具体值是多少。默认为undefined
  • get: 当我们通过person.name访问name的值时,get将被调用。该方法可以自定义返回的具体值是多少。get默认值为undefined
  • set: 当我们通过person.name = 'Jake’设置name的值时,set方法将被调用。该方法可以自定义设置值的具体方式。- - set默认值为undefined

需要注意的是,不能同时设置value、writable 与 get、set的值。 我们可以通过Object.defineProperty方法来修改这些属性类型。

下面我们用一些简单的例子来演示一下这些属性类型的具体表现。

configurable

// 用普通的方式给person对象添加一个name属性,值为TOM
var person = {
    name: 'TOM'
}

// 使用delete删除该属性
delete person.name;  // 返回true 表示删除成功

// 通过Object.defineProperty重新添加name属性
// 并设置name的属性类型的configurable为false,表示不能再用delete删除
Object.defineProperty(person, 'name', {
    configurable: false,
    value: 'Jake'  // 设置name属性的值
})

// 再次delete,已经不能删除了
delete person.name   // false

console.log(person.name)    // 值为Jake

// 试图改变value
person.name = "alex";
console.log(person.name) // Jake 改变失败
 
enumerable
var person = {
    name: 'TOM',
    age: 20
}

// 使用for-in枚举person的属性
var params = [];

for(var key in person) {
    params.push(key);
}

// 查看枚举结果
console.log(params);  // ['name', 'age']

// 重新设置name属性的类型,让其不可被枚举
Object.defineProperty(person, 'name', {
    enumerable: false
})

var params_ = [];
for(var key in person) {
    params_.push(key)
}

// 再次查看枚举结果
console.log(params_); // ['age']
 
writable
var person = {
    name: 'TOM'
}

// 修改name的值
person.name = 'Jake';

// 查看修改结果
console.log(person.name); // Jake 修改成功

// 设置name的值不能被修改
Object.defineProperty(person, 'name', {
    writable: false
})

// 再次试图修改name的值
person.name = 'alex';

console.log(person.name); // Jake 修改失败
 
value
var person = {}

// 添加一个name属性
Object.defineProperty(person, 'name', {
    value: 'TOM'
})

console.log(person.name)  // TOM
 
get/set
var person = {}

// 通过get与set自定义访问与设置name属性的方式
Object.defineProperty(person, 'name', {
    get: function() {
        // 一直返回TOM
        return 'TOM'
    },
    set: function(value) {
        // 设置name属性时,返回该字符串,value为新值
        console.log(value + ' in set');
    }
})

// 第一次访问name,调用get
console.log(person.name)   // TOM

// 尝试修改name值,此时set方法被调用
person.name = 'alex'   // alex in set

// 第二次访问name,还是调用get
console.log(person.name) // TOM

请尽量同时设置get、set。如果仅仅只设置了get,那么我们将无法设置该属性值。如果仅仅只设置了set,我们也无法读取该属性的值。

Object.defineProperty只能设置一个属性的属性特性。当我们想要同时设置多个属性的特性时,需要使用我们之前提到过的Object.defineProperties

var person = {}

Object.defineProperties(person, {
   name: {
       value: 'Jake',
       configurable: true
   },
   age: {
       get: function() {
           return this.value || 22
       },
       set: function(value) {
           this.value = value
       }
   }
})

person.name   // Jake
person.age    // 22

读取属性的特性值

我们可以使用Object.getOwnPropertyDescriptor方法读取某一个属性的特性值。

var person = {}

Object.defineProperty(person, 'name', {
    value: 'alex',
    writable: false,
    configurable: false
})

var descripter = Object.getOwnPropertyDescriptor(person, 'name');

console.log(descripter);  // 返回结果如下

descripter = {
    configurable: false,
    enumerable: false,
    value: 'alex',
    writable: false
}

尾语:点赞是一种鼓励,关注是一种陪伴。让我们在彼此的分享中,感受世界的温暖和力量!


  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
第1节 python高级编程.zip 是一门关于Python编程语言的高级应用的教程。Python是一种易于学习且功能强大的编程语言,广泛应用于各行各业。这门课程将帮助学习者掌握Python的高级编程技巧,包括函数式编程、面向对象编程、装饰器等。通过学习这门课程,学习者可以进一步提升自己的Python编程能力,更加熟练地应用Python解决实际问题。 第2节 linux系统编程.zip 是一门关于Linux操作系统的系统级编程的课程。Linux操作系统是一种开源的、免费的操作系统,广泛应用于服务器、嵌入式设备等领域。该课程将帮助学习者理解Linux操作系统的基本原理和核心概念,并教授如何进行系统级编程,包括文件操作、进程管理、内存管理等。学习这门课程可以帮助学习者深入理解Linux系统,并掌握开发Linux应用程序的技能。 第3节 网络编程.zip 是一门关于网络编程的课程。在现代社会中,网络已经成为人与人、人与机器之间进行信息交流和数据传输的重要手段。这门课程将帮助学习者理解网络编程的基本原理和核心技术,并通过实例演示如何使用Python语言进行网络编程。学习者将学习如何使用Socket编程进行网络通信、如何实现客户端和服务器端的交互、如何处理网络通信中的异常等。通过学习这门课程,学习者可以掌握网络编程的基本技能,为开发网络应用程序打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值