前端基础整理 | Javascript基础 (二)对象与继承

个人向,对JS知识进行了查漏补缺,主要来源于《JS高级程序设计》和网上博客,本文内容主要包括以下:

  1. 对象
  2. 创建对象
  3. 继承

一、对象

特性(attribute),描述了属性(property)的各种特征。内部使用,不能直接访问,两对方括号括起来。

1. 数据属性:

  • 定义:包含一个数据值的位置,在这个位置可以对数据值进行读写。
  • 创建方法:定义对象的时候的键值对就是数据属性啦。
  • 特性:
    • [[Configurable]] :表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true
    • [[Enumerable]] :表示能否通过for-in循环返回属性,默认为 true
    • [[Writable]] :表示能否修改属性的值,默认为 true
    • [[Value]] :包含该属性的数据值。默认为 undefined
  • 设置属性方法:
    Object.defineProperty(person, 'name', {
        configurable: true, //可以被删除,可以修改特性,可以修改为访问器属性
        writable: false, //不可以写入其他值
        enumerable: true, //可以for-in遍历
        value: 'tony' //值是tony
    })
    Object.getOwnPropertyDescriptor(person,'name').configurable // 查看特性
    

❗在使用defineProperty创建时候,未定义configurable / writable / enumerable都是默认false

2. 访问器属性

  • 创建方法:不能直接定义,只能通过Object.defineProperty()方法来定义。
  • 特性:
    • [[Configurable]] :表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true
    • [[Enumerable]] :表示能否通过for-in循环返回属性,默认为true
    • [[Get]] :读取属性调用的函数,默认undefined
    • [[Set]] :写入属性调用的函数,默认undefined
  • 设置属性方法:
    var person = {
      _age:10,
      isAdult: false
    }
    Object.defineProperty(person, 'age', {
      get: function () { // 只指定getter那默认不能write只能read
        return this._age
      },
      set: function (val) { //只指定setter那默认不能read只能write
        this._age = val
        if (val > 18)
          this.isAldult = true
        else
          this.isAldult = false
      }  
    })
    
  • 定义多个属性
    Object.defineProperties(book, {
      _name: {
        writable: true,
        configurable: true,
        value: 'tony'
      },
      name: {
        get: function(){},
        set: function(){}
      }
    })
    

二、创建对象

1. 工厂模式:

  • 优点:解决了创建多个相似对象;
  • 缺点:但问题是无法识别对象的类型。
function createPerson(name, age) {
  let o = new Object()
  o.name = name
  o.age = age
  o.sayName = function() { alert(o.name); }
  return o;
}

2. 构造函数模式

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

实际上,任何函数都可以是构造函数,只要配上new。而构造函数没有用new来调用,也是一个普通函数。

  • 那么new做了什么呢?
  1. 创建一个新对象
  2. 把构造函数的作用域赋给新对象(this指向新对象)
  3. 执行构造函数代码(给新对象添加属性)
  4. 返回新对象

手动模拟一下new的工作:

function newPerson(name, age) {
  let o = new Object()
  Person.call(o, name, age)
  return o
}
tony = newPerson('tony', 10)
  • 缺点
    构造函数也存在问题:每个方法都在实例上重新创建一遍。可以用这段代码证明:console.log(person1.sayName === person2.sayName) // false
    当然,我们可以在外部声明函数,然后在构造函数中引用该函数。但是这么做会在全局作用域定义很多函数,封装性大大降低。而原型模式能很好地解决这一点,因为它可以让所有对象实例共享它所包含的属性和方法。

3. 原型模式

  • 原型(prototype)是什么?
    prototype是一个指针,指向函数原型对象。prototype是一个函数的属性。
    (理解原型的前提是要知道,函数本身也是一个对象,prototype是它的属性之一,指向一个叫原型对象的东西)

  • 一张经典的图片

    一张经典的图片

  • prototype 与 __proto__
    当创建函数的时候,函数的原型对象自动获得一个constructor属性,该属性指向这个函数。
    当创建实例的时候,实例内部也包括一个指针[[Prototype]],指向构造函数的原型对象。这个东西在chrome之类的浏览器实现为__proto__指针。在ES5中标准的拿实例对象原型的方法是Object.getPrototypeOf()

    三种原型对象的取法

  • 原型对象挂载方法和属性
    我们可以向原型对象上挂属性和方法,这样每个使用这个原型的实例都能读到。并且,我们解决了构造函数方法每次实例化都创建的问题。

    image.png

  • 判断属性方法

person.hasOwnProperty('name') //true因为这是来自实例的属性。
person.hasOwnProperty('age') //false因为这是来自原型继承来的属性。
'age' in person //true 'in'操作符在对象能访问该属性时返回true
  • 获取属性方法
Object.keys(Person.prototype) //获取[[Enumerable]]为true的可枚举实例属性
Object.getOwnPropertyNames() //获取所有的实例属性

Reflect.ownKeys(person) // 获取所有的实例属性以及symbol
Object.getOwnPropertyNames(person).concat(Object.getOwnPropertySymbols(person)) // 就是上面代码的实际返回
  • 缺点
    原型对象的缺点是:省略了为构造函数传递初始化参数的环节,导致默认情况下取得相同属性

4. 原型+构造函数模式

目前ECMAScript中最广泛的模式,就是构造函数模式用来定义实例属性,原型模式用来定义方法和共享的属性。

5. 动态原型模式

其实就是在构造函数内弄了一个判断语句,当不存在一个方法的时候,将方法挂在原型上。

function Person(name) {
  this.name = name
  if ( typeof this.sayName!='function') {
    Person.prototype.sayName = function() { alert(this.name)}
  }
}

6.寄生构造模式

代码和工厂模式一样,就是用new来创建,暂时不做分析

7.稳妥构造模式

有种闭包的感觉,不用this进行构造,没有公共属性,用在需要特殊的安全执行环境。

三、 继承

1. 原型链

MDN文档的描述再结合上面的"一张经典的图片"食用更佳:
原型链的顶端是Object,再往上就是null了,null没有原型。

JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

2. 借用构造函数继承

  • 核心:
    子类调用父类的构造函数从而实现属性的继承
  • 优点:
    1.可以向父类传递参数
    2.父类的引用属性不会被子类实例共享
  • 缺点:
    1.父类方法不能复用
    2.对子类实例使用 instanceof 只会识别到子类
function Person(name){
  this.name = name
}
function Student(name){
  Person.call(this, name)
}

3. 原型链继承

  • 核心:子类把prototype指向父类的一个实例对象
  • 优点:1.父类方法可复用;2. instanceof 可以识别到父类子类
  • 缺点:1.子类构建实例时不能传参;2. 父类的引用属性会被所有子类实例共享
function Person(name){
  this.name = name
}
person = new Person('tony')
function Student(){}
Student.prototype = person 
Student.prototype.constructor = Student

4. 原型链+构造函数 组合继承

  • 核心:子类调用父类构造函数来实现属性继承,prototype指向父类的实例对象。
  • 优点:构造函数和原型链互补,即父类方法可复用 & 父类引用类型属性不会被共享。
  • 缺点:构造函数调用了两次,造成性能浪费,并且可能会覆盖子类同名属性。
function Person(name){
  this.name = name
}
Person.prototype.sayHi = function(){alert('hello')}
function Student(name){
  Person.call(this,name)
}
Student.prototype = new Person()
Student.prototype.constructor = Student

5. 原型式继承

  • 核心:创建临时构造函数,把传入对象作为构造函数的原型,返回临时类型的新实例。
function object(o){
  function F(){}
  F.prototype = o
  return new F()
}
let person = {
    name: 'tony'
};
let anotherPerson = object(person)

ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。

所以上述可以简化为 let anotherPerson = Object.create(person)

6. 寄生继承

只是一种思路而已,没什么优点,通过给使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

function createAnother(original){ 
    var clone=object(original)
    clone.sayHi = function(){
        alert("hi");
    };
    return clone
}
var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person)
anotherPerson.sayHi()

7. 寄生组合式继承

目前最完美的继承方法,只需要在继承函数中调用构造函数再使用下面的继承就行了。

function inheritPrototype(subType, superType){
    var prototype = Object.create(superType.prototype); // 创建了父类原型的浅复制
    prototype.constructor = subType;             // 修正原型的构造函数
    subType.prototype = prototype;               // 将子类的原型替换为这个原型
}

为了方便理解,这里有两个类似的继承函数。第一个是使用类似原型构造的F函数,第二个是直观的展示了继承在Chrome等具有__proto__指针中的形式。

function F_inherits(Child, Parent) {
  var F = function() {}
  F.prototype = Parent.prototype
  Child.prototype = new F()
  Child.prototype.constructor = Child
}
function myInherits(Child, Parent) {
  Child.prototype = { constructor: Child, __proto__: Parent.prototype }
}

8. class 继承(ES6)

ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

  • 语法:
class A {}
class B extends A {
  constructor() {
    super();
  }
}
  • 实现原理:
class A {}
class B {}
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

ES6继承与ES5继承的异同:

  • 相同点:本质上ES6继承是ES5继承的语法糖
  • 不同点:
    1. ES6继承中子类的构造函数的原型链指向父类的构造函数,ES5中使用的是构造函数复制,没有原型链指向。
    2. ES6子类实例的构建,基于父类实例,ES5中不是。

四、一些自己实现的函数

帮助大家更好地理解:对象、继承。

/**
 * call实现
 */
Function.prototype._call = function(ctx, ...args) {
  ctx = ctx || window
  ctx.func = this
  let result = ctx.func(...args)
  delete ctx.func
  return result
}
/**
 * apply实现
 */
Function.prototype._apply = function(ctx, args) {
  ctx = ctx || window
  ctx.func = this
  let result = ctx.func(...args)
  delete ctx.func
  return result
}
/**
 * bind实现
 */
Function.prototype._bind = function(target) {
  target = target || window
  const that = this
  const args = [...arguments].slice(1)
  let fn = function() {
    return that.apply(
      this instanceof fn ? this : target,
      args.concat(...arguments)
    )
  }
  let F = function() {}
  F.prototype = this.prototype
  fn.prototype = new F() // fn.prototype.__proto__ == this.prototype  true
  return fn
}
/**
 * instanceof实现
 */
function _instanceof(a, b) {
  let prototype = b.prototype
  let a = a.__proto__
  while (true) {
    if (a === null || a === undefined) {
      return false
    } else if (a === prototype) {
      return true
    } else {
      a = a.__proto__
    }
  }
}

你可能感兴趣的:(前端基础整理 | Javascript基础 (二)对象与继承)