重学js(原型链和面向对象的特性)

221 阅读8分钟

面向对象有三大特性:封装、继承、多态 (抽象)

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
  • 多态:不同的对象在执行时表现出不同的形态; (抽象)把现实事物抽象成代码的过程

继承

继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

JavaScript当中如何实现继承呢?

不着急,我们先来看一下JavaScript原型链的机制; p再利用原型链的机制实现一下继承;

原型链(prototype chain)

从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:

1650789900(1).png

那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性呢?

1650768626(1).png 我们会发现它打印的是 [Object: null prototype] {}
事实上这个原型就是我们最顶层的原型了 p从Object直接创建出来的对象的原型都是 [Object: null prototype] {}

[Object: null prototype] {} 原型有什么特殊吗?

  • 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
  • 特殊二:该对象上有很多默认的属性和方法;

通过原型链实现继承

// 父类: 公共属性和方法
function Person() {
  this.name = "why"
  this.friends = []
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student() {
  this.sno = 111
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}


// name/sno
var stu = new Student()

// console.log(stu.name)
// stu.eating()

// stu.studying()


// 原型链实现继承的弊端:
// 1.第一个弊端: 打印stu对象, 继承的属性是看不到的
// console.log(stu.name)

// 2.第二个弊端: 创建出来两个stu的对象
var stu1 = new Student()
var stu2 = new Student()

// 直接修改对象上的属性, 是给本对象添加了一个新属性
stu1.name = "kobe"
console.log(stu2.name)

// 获取引用, 修改引用中的值, 会相互影响
stu1.friends.push("kobe")

console.log(stu1.friends)
console.log(stu2.friends)

// 3.第三个弊端: 在前面实现类的过程中都没有传递参数
var stu3 = new Student("lilei", 112)

image.png

继承-借用构造函数方案

// 父类: 公共属性和方法
function Person(name, age, friends) {
  // this = stu
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends)
  // this.name = name
  // this.age = age
  // this.friends = friends
  this.sno = 111
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}


// name/sno
var stu = new Student("why", 18, ["kobe"], 111)

// console.log(stu.name)
// stu.eating()

// stu.studying()


// 原型链实现继承已经解决的弊端
// 1.第一个弊端: 打印stu对象, 继承的属性是看不到的
console.log(stu)

// 2.第二个弊端: 创建出来两个stu的对象
var stu1 = new Student("why", 18, ["lilei"], 111)
var stu2 = new Student("kobe", 30, ["james"], 112)

// // 直接修改对象上的属性, 是给本对象添加了一个新属性
// stu1.name = "kobe"
// console.log(stu2.name)

// // 获取引用, 修改引用中的值, 会相互影响
stu1.friends.push("lucy")

console.log(stu1.friends)
console.log(stu2.friends)

// // 3.第三个弊端: 在前面实现类的过程中都没有传递参数
// var stu3 = new Student("lilei", 112)



// 强调: 借用构造函数也是有弊端:
// 1.第一个弊端: Person函数至少被调用了两次
// 2.第二个弊端: stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要
 

image.png

继承-父类原型赋值给子类

// 父类: 公共属性和方法
function Person(name, age, friends) {
  // this = stu
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends)
  // this.name = name
  // this.age = age
  // this.friends = friends
  this.sno = 111
}

// 直接将父类的原型赋值给子类, 作为子类的原型
Student.prototype = Person.prototype

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}


// name/sno
var stu = new Student("why", 18, ["kobe"], 111)
console.log(stu)
stu.eating()

不正确 student和Person的原型指向同一个 给student原型加东西Person也会出现

继承-原型式继承-对象

var obj = {
  name: "why",
  age: 18
}

var info = Object.create(obj)

// 原型式继承函数
//方法一
function createObject1(o) {
  var newObj = {}
  Object.setPrototypeOf(newObj, o)
  return newObj
}
//方法二
function createObject2(o) {
  function Fn() {}
  Fn.prototype = o
  var newObj = new Fn()
  return newObj
}

// var info = createObject2(obj)
//方法三
var info = Object.create(obj)
console.log(info)
console.log(info.__proto__)

继承-寄生式继承-对象


var personObj = {
  running: function() {
    console.log("running")
  }
}

function createStudent(name) {
  var stu = Object.create(personObj)
  stu.name = name
  stu.studying = function() {
    console.log("studying~")
  }
  return stu
}

var stuObj = createStudent("why")
var stuObj1 = createStudent("kobe")
var stuObj2 = createStudent("james")

继承-寄生组合式继承(最终方案)

function createObject(o) {
  function Fn() {}
  Fn.prototype = o
  return new Fn()
}

function inheritPrototype(SubType, SuperType) {
  SubType.prototype = Objec.create(SuperType.prototype)
  Object.defineProperty(SubType.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: SubType
  })
}

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

Person.prototype.running = function() {
  console.log("running~")
}

Person.prototype.eating = function() {
  console.log("eating~")
}


function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends)
  this.sno = sno
  this.score = score
}

inheritPrototype(Student, Person)

Student.prototype.studying = function() {
  console.log("studying~")
}

var stu = new Student("why", 18, ["kobe"], 111, 100)
console.log(stu)
stu.studying()
stu.running()
stu.eating()

console.log(stu.constructor.name)


对象的方法补充

Object.create()


使用指定的原型对象和属性创建一个新对象。
var obj = {
  name: "why",
  age: 18
}

var info = Object.create(obj, {
  address: {
    value: "北京市",
    enumerable: true
  }
})
//增加在info上而不是在原型增加

hasOwnProperty是否是自有属性

// hasOwnProperty方法判断
// console.log(info.hasOwnProperty("address"))
// console.log(info.hasOwnProperty("name"))

in 操作符: 不管在当前对象还是原型中返回的都是true

// console.log("address" in info)
// console.log("name" in info)

// // for in
// for (var key in info) {
//   console.log(key)
// }

instanceof 用于检测构造函数的pototype,是否出现在某个实例对象的原型链上

function createObject(o) {
  function Fn() {}
  Fn.prototype = o
  return new Fn()
}

function inheritPrototype(SubType, SuperType) {
  SubType.prototype = createObject(SuperType.prototype)
  Object.defineProperty(SubType.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: SubType
  })
}


function Person() {

}

function Student() {

}

inheritPrototype(Student, Person)

console.log(Person.prototype.__proto__)

var stu = new Student()
console.log(stu instanceof Student) // true
console.log(stu instanceof Person) // true
console.log(stu instanceof Object) // true

isPrototypeOf 用于检测某个对象,是否出现在某个实例对象的原型链上

function Person() {

}

var p = new Person()

console.log(p instanceof Person)
console.log(Person.prototype.isPrototypeOf(p))

// 
var obj = {
  name: "why",
  age: 18
}

var info = Object.create(obj)

// console.log(p instanceof Person)//只能判断对象是否属于函数
// console.log(info instanceof obj) //错误的
//console.log(person.prototype.isPrototypeOf(info))
console.log(obj.isPrototypeOf(info))

Object.assign()创建一个新对象,使用现有的对象来提供新创建的对象

因为 Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。

const target = { a: 1, b: 2 }; const source = { c: 4, d: 5 }; 
// const res = Object.assign(target, source) 
// 原对象target也会发生变化 
// 解决方案1 const res1 = Object.assign({}, target, source) 
// 解决方案2 const res2 = {...target, ...source}

//合并具有相同属性的对象
const o1 = { a: 1, b: 1, c: 1 }; 
const o2 = { b: 2, c: 2 }; 
const o3 = { c: 3 }; 
const obj = Object.assign({}, o1, o2, o3); console.log(obj); 
// { a: 1, b: 2, c: 3 },注意属性被后续参数中具有相同属性的其他对象覆盖

//继承属性和不可枚举属性是不可拷贝的
const obj = Object.create({foo: 1}, 
{ // foo 是个继承属性。 bar: { value: 2 // bar 是个不可枚举属性。 },
baz: { value: 3, enumerable: true // baz 是个自身可枚举属性。 } });
const copy = Object.assign({}, obj); console.log(copy); // { baz: 3 }


//原始类型会包装为对象
const v1 = "abc"; const v2 = true;
const v3 = 10; 
const v4 = Symbol("foo") 
const v5=[7,8,9]
const v6=[{a1:1}

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 原始类型会被包装,null 和 undefined 会被忽略。 
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4,v5,v6); 
console.log(obj); //{ '0': { a1: 1 }, '1': 8, '2': 9 }

//拷贝Symbol类型的属性
const o1 = { a: 1 };
const o2 = { [Symbol('foo')]: 2 }; 
const obj = Object.assign({}, o1, o2);
console.log(obj); // { a : 1, [Symbol("foo")]: 2 }
Object.getOwnPropertySymbols(obj); // [Symbol(foo)]

//异常会打断后续的拷贝
const target = Object.defineProperty({}, "foo", { value: 1, writable: false }); 
// target 的 foo 属性是个只读属性。 
Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4}); // TypeError: "foo" is read-only // 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。
console.log(target.bar);
// 2,说明第一个源对象拷贝成功了。 console.log(target.foo2); 
// 3,说明第二个源对象的第一个属性也拷贝成功了。 console.log(target.foo); 
// 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。 console.log(target.foo3); 
// undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。 
console.log(target.baz); // undefined,第三个源对象更是不会被拷贝到的

//拷贝访问器
const obj = {
  foo: 1,
  get bar() {
    return 2
  },
}
let copy = Object.assign({}, obj)
console.log(copy) // { foo: 1, bar: 2 } copy.bar的值来自obj.bar的getter函数的返回值
// 下面这个函数会拷贝所有自有属性的属性描述符
function completeAssign(target, ...sources) {
  sources.forEach((source) => {
    let descriptors = Object.keys(source).reduce((descriptors, key) => {
      descriptors[key] = Object.getOwnPropertyDescriptor(source, key)
      return descriptors
    }, {})
    // Object.assign 默认也会拷贝可枚举的Symbols
    Object.getOwnPropertySymbols(source).forEach((sym) => {
      let descriptor = Object.getOwnPropertyDescriptor(source, sym)
      if (descriptor.enumerable) {
        descriptors[sym] = descriptor
      }
    })
    Object.defineProperties(target, descriptors)
  })
  return target
}
copy = completeAssign({}, obj)
console.log(copy)
// { foo:1, get bar() { return 2 } }

Object.defineProperty()  方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: false
});

object1.property1 = 77;
// throws an error in strict mode

console.log(object1.property1);
// expected output: 42

Object.defineProperties(obj, props) 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。


var obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
  // etc. etc.
});

Object.entries() 方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。


const object1 = {
  a: 'somestring',
  b: 42
};

for (const [key, value] of Object.entries(object1)) {
  console.log(`${key}: ${value}`);
}

// expected output:
// "a: somestring"
// "b: 42"

Object.freeze()  方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

const obj = {
  prop: 42
};

Object.freeze(obj);

obj.prop = 33;
// Throws an error in strict mode

console.log(obj.prop);
// expected output: 42

原型继承关系

1650878429(1).png

var obj = {
  name: "why"
}

console.log(obj.__proto__)

// 对象里面是有一个__proto__对象: 隐式原型对象

// Foo是一个函数, 那么它会有一个显示原型对象: Foo.prototype
// Foo.prototype来自哪里?
// 答案: 创建了一个函数, Foo.prototype = { constructor: Foo }

// Foo是一个对象, 那么它会有一个隐式原型对象: Foo.__proto__
// Foo.__proto__来自哪里?
// 答案: new Function()  Foo.__proto__ = Function.prototype
// Function.prototype = { constructor: Function }

// var Foo = new Function()
function Foo() {

}

console.log(Foo.prototype === Foo.__proto__)//false
console.log(Foo.prototype.constructor)//Foo
console.log(Foo.__proto__.constructor)//Function


var foo1 = new Foo()
var obj1 = new Object()
console.log(Function.prototype ===Function.__proto__)true
console.log(Object.getOwnPropertyDescriptors(Function.__proto__))true