前端基础整理 | 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__
    }
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 博客内容:什么是面向对象为什么要面向对象面向对象编程的特性和原则理解对象属性创建对象继承 什么是面向对象 面向对象...
    _Dot912阅读 1,398评论 3 12
  •   面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意...
    霜天晓阅读 2,092评论 0 6
  • 第3章 基本概念 3.1 语法 3.2 关键字和保留字 3.3 变量 3.4 数据类型 5种简单数据类型:Unde...
    RickCole阅读 5,096评论 0 21
  • JavaScript面向对象程序设计 本文会碰到的知识点:原型、原型链、函数对象、普通对象、继承 读完本文,可以学...
    moyi_gg阅读 757评论 0 2
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,521评论 0 5