熟练掌握JavaScript面向对象与原型【class & OOP】

135 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

前言

JavaScript的原型和原型链,类和继承,是JavaScript的核心重点,理解了对像的这些关系,我们后面看优秀的代码框架是就从大的方面入手,可以层层深入分析。基于原型是JavaScript的高级核心部分,ES6中的Clas面向对象也是居于原型链的语法糖。

原型和原型链

JavaScript中的对象

像下面第一的字面量对象obj对象一样,JavaScript中对象一般是键值对的形式, 健的类型有限制,值可以是任意类型,如果要以哈希的形式使用的话还是建议用Map对象,可参考下面文章。

JavaScriptMap对象和obj对象的区别,map的哈希表现

// 创建对象字面量
let obj = {
  prop1: 123,
  prop2: 'abc',
  prop3: [1, 2, 3],
  prop4: function () { },
  prop5: {}
};

可以像下面这样添加一个全新的属性

let obj = {
  prop1: 123,
  prop2: 'abc',
  prop3: [1, 2, 3],
  prop4: function () { },
  prop5: {}
};

obj.prop6 = 'hello world'; // (1)新增属性

console.log(obj);

image.png 为什么说JavaScript是基于对象的和原型的看下面结果

console.log(Array instanceof Object); // true
console.log(String instanceof Object); // true
console.log(Function instanceof Object); // true
console.log(Number instanceof Object); // true
console.log(Math instanceof Object); // true
console.log(RegExp instanceof Object);  // true
console.log(Boolean instanceof Object);  // true
console.log(Symbol instanceof Object);  // true
console.log(Date instanceof Object); // true

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

\

理解原型

代码世界的思想里,重复造轮子是要避免的,而轮子的最小元素就是像属性和方法了,而OOP思想中继承这一方法就有这样的功能,而JavaScript中原型就是有扩展和复用的继承属性,所以要重视。

使用setPrototypeOf() 设置对象的方式实现继承

liek this 没有设置tom对象为gogo 对象的原型时,gogo访问eglish属性时undefinedObject.setPrototypeOf(gogo, tom) 后可以访问english属性,并打印出'hello world'

const tom = { english: 'hello world' };
const jack ={ china: '你好世界'}
const gogo = { language: 'gogo'}

console.log(gogo.english); // undefined

Object.setPrototypeOf(gogo, tom)

console.log(gogo.english); // 'hello world'

在上面代码的基础上,我们可以看到,gogo的原型只能是tom或者jack, 这次使用Object.setPrototypeOf(gogo, jack)的时候,就变成这样了console.log(gogo.english); // undefined

const tom = { english: 'hello world' };
const jack ={ china: '你好世界'}
const gogo = { language: 'gogo'}
console.log(gogo.english); // undefined
Object.setPrototypeOf(gogo, tom)
console.log(gogo.english); // 'hello world'

// 新增部分
console.log(gogo.china);  // undefined
Object.setPrototypeOf(gogo, jack)
console.log(gogo.china); // 你好世界
console.log(gogo.english);  // undefined

不用担心,我们可以这样做 我们只需要在下面【1】的地方改为让tom的原型为Jack,而gogo的原型为tom,这样gogo就可以顺着网线去找你了哈哈调皮了, 就可以顺着原型链找到对应的属性了, 下面我们还会详细的说说啥是原型链

const tom = { english: 'hello world' };
const jack ={ china: '你好世界'}
const gogo = { language: 'gogo'}

console.log(gogo.english); // undefined

Object.setPrototypeOf(gogo, tom)

console.log(gogo.english); // 'hello world'

console.log(gogo.china);
Object.setPrototypeOf(tom, jack) // 【1】------- 让tom ,继承jack-----
console.log(gogo.china); // 你好世界
console.log(gogo.english);  // hello world

注意区分 prototype 是对象的原型,而[[Prototype]] __proto__是对象的原型对象属性

使用Object.create()实现继承

使用 Object.create(null) 创建对象会创建一个完全的空对象,

// 创建一个原型为 null 的空对象
let o = Object.create(null);

要区别这两种方式

let o = {};
// 以字面量方式创建的空对象就相当于:
let o = Object.create(Object.prototype);

例如:

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  },
  foo: 123,
  bar: 'abc',
  sing() {
    return 'I can sing'
  }
};

const me = Object.create(person);

me.name = 'Matthew'; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction();
console.log(me.foo); // 123
console.log(me.sing()); // I can sing
// expected output: "My name is Matthew. Am I human? true"

构造函数与原型

我们可以定义一个Person构造函数,然后它又nameage属性,有唱舞rap和selfIntroduction的方法。,有点和面向对象的class 方式相似, 我们通过new 一个新的对象,实现了对象的继承,我们只需要定义一个构造函数,后面就可以new很多个对象了.

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

Person.prototype.sing = function () {
  return 'I can sing'
}

Person.prototype.dance = function () {
  return 'I can dance'
}

Person.prototype.rap = function () {
  return 'I can rap'
}

Person.prototype.selfIntroduction = function () {
  return `Hi I'am ${this.name}, ${this.age} years old this year, ${this.sing()},${this.dance()}, ${this.rap()}`
}

let me = new Person('周杰伦', 18)
let you = new Person('周星驰', 28)
let she = new Person('王心凌', 18)

console.log(me.sing()); // I can sing
console.log(you.selfIntroduction()); // Hi I'am 周星驰, 28 years old this year, I can sing,I can dance, I can rap
console.log(she.selfIntroduction()); // Hi I'am 王心凌, 18 years old this year, I can sing,I can dance, I can rap

手写一个new方法

new 关键字会进行如下的操作:

  • 1、创建一个空的简单JavaScript对象(即{});
  • 2、为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  • 3、将步骤1新创建的对象作为this的上下文 ;
  • 4、如果该函数没有返回对象,则返回this。、
  function my_new(fn) {

    let obj = {} // (1)

    if (fn.prototype !== null) {
      obj.__proto__ = fn.prototype // (2)
    }

    let res = fn.apply(obj, Array.prototype.slice.call(arguments, 1)) // (3)

    if ((typeof res === 'object' || typeof res === 'function') && res !== null) {
      return res
    }

    return obj

  }

// test

function Foo(){
  this.a = 123; // 123
  this.b = 'hello'
}

let test = my_new(Foo)
console.log(test.a, test.b); // 123 hello

let res = new Foo()
console.log(res.a, res.b); // 123 hello

理解原型链

先看两张图丫丫金,

image.png

好理解吗

image.png 这样会不会明白一些

function Foo() {}
Foo.prototype.sayHi = function () {
  return 'Hello'
}

let foo = new Foo()

console.log(foo.__proto__ === Foo.prototype); // true
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Object.__proto__); // true

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

理解原型链,需要动手实践一遍,就可以理解清楚了,在控制台执行里面代码,然后一层层拨开原型链的面纱,找到它们的继承关系,

 function Foo() { }
    Foo.prototype.sayHi = function () {
      return 'Hello'
    }
    Foo.prototype.money = '1000000'
    Function.prototype.naiveFunc = function () {
      return 'I am Function'
    }

    Object.prototype.navieObj = function () {
      return 'I ma navieObj'
    }

    let foo = new Foo()

    console.log(foo.navieObj()); // I ma navieObj
    console.log(Foo.naiveFunc()); // I am Function
    
    console.log('-------------foo-------------------');
    console.log('foo:',foo);
    console.log('foo.constructor:',foo.constructor);
    console.log('foo.__proto__:',foo.__proto__);
    console.log('foo.prototype:',foo.prototype);
    // console.log(foo.prototype.__proto__); error

    console.log('-------------Foo-------------------');
    console.log('Foo:',Foo);
    console.log('Foo.__proto__:',Foo.__proto__);
    console.log('Foo.constructor:',Foo.constructor);
    console.log('Foo.prototype:',Foo.prototype);
    console.log('Foo.prototype.__proto__:',Foo.prototype.__proto__);

    console.log('-------------Function-------------------');
    console.log('Function:',Function);
    console.log('Function.__proto__:',Function.__proto__);
    console.log('Function.constructor:',Function.constructor);
    console.log('Function.prototype:',Function.prototype);
    console.log('Function.prototype.__proto__',Function.prototype.__proto__);
    
    console.log('-------------Object-------------------');
    console.log('Object:',Object);
    console.log('Object.constructor:',Object.constructor);
    console.log('Object.__proto__:',Object.__proto__);
    console.log('Object.prototype:',Object.prototype);
    console.log('Object.prototype.__proto__:',Object.prototype.__proto__);

执行后结果是这样的

image.png 我们展开foo的原型对象属性foo.__proto__如下

image.png

它们的指向关系简化一下就是:

foo.__proto__ ->Foo.prototype -> Foo.prototype.__proto__ ->Object.prototype -> Object.prototype.__proto__ -> null

对象的继承查找路线大概是这样,查找某个值的时候,如果一条链查完都没有,就返回null

类和继承

类的定义

我们看看下面类的定义,是不是和上面的构造函数很像,这里我们注意到constructorstatic 关键字。 constructor 其实就是Person的构造函数,static是来定义方法的关键字,相当于把方法添加到构造函数这个对象里,其他的方法是添加到对象的原型上的。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static konfu() {
    return 'I can play konfu'
  }
  sing() {
    return 'I can sing'
  }
  dance() {
    return 'I can dance'
  }
  rap() {
    return 'I can rap'
  }
  selfIntroduction() {
    return `I am ${this.name}, ${this.age} years old this year`
  }
}

let person = new Person('李白', 20)
console.log(person.selfIntroduction()); // I am 李白, 20 years old this year
console.log(Person.konfu()); // I can play konfu
console.log(person.konfu());  // error

class 其实是语法糖,转化编译的时候,其实代码会变成下面这样

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

Person.konfu = function () {} // (1) 使用static 关键字声明的静态方法

Person.prototype.sing = function () {} // 普通原型上的方法
.
.
.

类的继承

class 继承参考文章 通过extends实现对父类的继承

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static konfu() {
    return 'I can play konfu'
  }
  sing() {
    return 'I can sing'
  }
  dance() {
    return 'I can dance'
  }
  rap() {
    return 'I can rap'
  }
  selfIntroduction() {
    return `I am ${this.name}, ${this.age} years old this year`
  }
}

class Monkey extends Person {
  eatBanana() {
    return 'I can eat banana'
  }
}

let person = new Person('李白', 20)
let monkey = new Monkey('美猴王', 500)
console.log(person.selfIntroduction()); // I am 李白, 20 years old this year
console.log(monkey.selfIntroduction()); // I am 美猴王, 500 years old this year
console.log(monkey.eatBanana()); // I can eat banana
console.log(person.eatBanana()); // error

总结

JavaScript的原型和原型链,类和继承,是JavaScript的核心重点,理解了对像的这些关系,我们后面看优秀的代码框架是就从大的方面入手,可以层层深入分析。 这里介绍的还不够全面,后面找时间,把原型和原型链,类和继承的细节部分,在做深入。

看完三件事!!!

graph TD
点赞 --> 关注 --> 评论