【进阶第 6 期】 JS对象、原型 Prototype及继承

287 阅读11分钟

前言

本文主要回答以下几个问题:

  • 1、什么是对象?
  • 2、如何使用对象(创建以及访问,新增、删除、修改属性)?
  • 3、原型链及继承?

什么是对象?

对象究竟是什么?什么叫面向对象编程?

  • 对象(object),是面向对象(Object Oriented)中的术语,既表示客观世界问题空间(Namespace)中的某个具体的事物,又表示软件系统解空间中的基本元素。
  • 在软件系统中,对象具有唯一的标识符,对象包括属性(Properties)和方法(Methods),属性就是需要记忆的信息,方法就是对象能够提供的服务。在面向对象(Object Oriented)的软件中,对象(Object)是某一个类(Class)的实例(Instance)。 —— 维基百科

1、JavaScript 对象的特征

在 JavaScript 中,对象的状态和行为其实都被抽象为了属性。 为了提高抽象能力,JavaScript 的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。

2、JavaScript 对象的两类属性

对 JavaScript 来说,属性并非只是简单的名称和值,JavaScript 用一组特征(attribute)来描述属性(property)。

数据属性

数据属性具有四个特征:

属性描述
value属性的值
writable决定属性能否被赋值
enumerable决定 for in 能否枚举该属性
configurable决定该属性能否被删除或者改变特征值

通常用于定义属性的代码会产生数据属性,其中的 writable、enumerable、configurable 都默认为 true。我们可以使用内置函数 getOwnPropertyDescripter 来查看。

var o = { a: 1 };
o.b = 2;

//a和b皆为数据属性
Object.getOwnPropertyDescriptor(o,"a");
// {value: 1, writable: true, enumerable: true, configurable: true}

Object.getOwnPropertyDescriptor(o,"b");
// {value: 2, writable: true, enumerable: true, configurable: true}

访问器(getter/setter)属性 访问器属性的四个特征:

属性描述
getter函数或 undefined,在取属性值时被调用
setter函数或 undefined,在设置属性值时被调用
enumerable决定 for in 能否枚举该属性
configurable决定该属性能否被删除或者改变特征值

访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖。

对象系统

JavaScript 中的对象分类

宿主对象:由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。 内置对象:由 JavaScript 语言提供的对象。

  • 固有对象:由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
  • 原生对象:可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
  • 普通对象:由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。
  1. 宿主对象 在浏览器环境中,我们都知道全局对象是 window,window 上又有很多属性,如 document。这个全局对象 window 上的属性,一部分来自 JavaScript 语言,一部分来自浏览器环境。 JavaScript 标准中规定了全局对象属性,W3C 的各种标准中规定了 Window 对象的其它属性。

JavaScript 语言规定了全局对象的属性。

  1. 内置对象·固有对象 三个值:Infinity、NaN、undefined。 九个函数:eval、isFinite、isNaN、parseFloat、parseInt、decodeURI、decodeURIComponent、encodeURI、encodeURIComponent 一些构造器:Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeakSet、Function、Boolean、String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。 四个用于当作命名空间的对象:Atomics、JSON、Math、Reflect

  2. 内置对象·原生对象 JavaScript 中,能够通过语言本身的构造器创建的对象称作原生对象。

通过这些构造器,我们可以用 new 运算创建新的对象。几乎所有这些构造器的能力都是无法用纯 JavaScript 代码实现的,它们也无法用 class/extend 语法来继承。这些原生对象都是为了特定能力或者性能,而设计出来的“特权对象”。

特殊行为的对象 Array:Array 的 length 属性根据最大的下标自动发生变化。 Object.prototype:作为所有正常对象的默认原型,不能再给它设置原型了。 String:为了支持下标运算,String 的正整数属性访问会去字符串里查找。 Arguments:arguments 的非负整数型下标属性跟对应的变量联动。 模块的 namespace 对象:特殊的地方非常多,跟一般对象完全不一样,尽量只用于 import 吧。 类型数组和数组缓冲区:跟内存块相关联,下标运算比较特殊。 bind 后的 function:跟原来的函数相关联。

如何使用对象?

[一]声明对象

创建对象的方法有两种:

  • 对象字面量方法(创建对象的一种快捷方式:简化包含大量属性的对象的创建过程,即语法糖)。
  • 使用new 操作符后跟Object 类型的构造函数
//对象字面量方法--创建
var obj = {
    name:'foo',
    method:function{console.log(this.name);
    }
}
// 构造函数方法--创建
var  obj = new Object();//< == > obj = {}
obj.name = 'foo';
obj.method = function(){};

几种经典的模式;工厂模式,构造函数模式,原型模式,混合构造函数/原型模式,动态原型模式,寄生构造函数模式等。

工厂模式-->用函数来封装,以特定接口创建对象的细节

function person(name,age){
  var  obj = new Object();//通过Object构造器创建实例对象。
  obj.name = name;
  obj.age = age;
  obj.say = function(){
    console.log(this.name);
  };
  return obj;
}

var person1 =   person('zhangsan','18');
var person2 =   person('lisi','3');

console.log(instanceOf person1)//object
console.log(instanceof person2)//object
  • 解决了创建多个相似对象的问题,
  • 多用于创建多个含有相同属性,和方法的对象,避免代码的重复编写;
  • 没有解决对象识别的问题(即怎么知道一个对象的类型,无法通过instanceOf等判断区分,所有使用该模式创建的对象都是Object对象的一个实例)

构造函数模式

构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数习惯上首字母大写。另外就是调用方式的不同,普通函数是直接调用,而构造函数需要使用new关键字来调用。

var Car = function (model, year, miles) {
  this.model = model;
  this.year = year;
  this.miles = miles;
  this.run =function(){
    console.log(this.miles);
  }
};
var baoma = new Car("Tom", 2009, 20000);
var benchi = new Car("Dudu", 2010, 5000);
  • 解决对象识别的问题(即怎么知道一个对象的类型)
  • 缺点:每个方法都在每个实例上重新创建了一次,(如上面例子中 baoma.sayName !== benci.sayName)
  • 解决方式:提出构造函数,在全局写一个函数申明;(这种解决方式的缺点:全局函数只是局部调用,方法过多就得创建多个全局函数,没什么封装性可言);

原型模式

function Person() {}
    Person.name ='tom';
    Person.prototype.friends =['jerry','miqi','carry'];
    Person.prototype.logName = function() {
      console.log(this.name);
    }
}
var person1 = new Person();
person1.logName();//'tom'
//--可以for in 访问
for(key in person1) {
    console.log(key);
}

组合使用构造函数模式和原型模式

function Person(name,age) {
    this.name = name;
    this.age = age;
    this.friends = ['ajiao','pangzi'];
}
Person.prototype =
    constructor: Person,
    logName: function() {
        console.log(this.name);
    }
}
var person1 = new Person('evansdiy','22');
var person2 = new Person('amy','21');

person1.logName();//'evansdiy'
person1.friends.push('haixao');
console.log(person2.friends.length);//3

[二] 删除对象属性

// 仅删除属性值
obj.key = undefined
// 删的是属性名和属性值
delete obj.key

[三]查看对象的属性 (读属性)

1. 查看所有属性

// 1.1 查看所有属性
let obj = {name: 'Mia', age: 18}

Object.keys(obj)     // 查看自身属性名
Object.values(obj)   // 查看自身属性值
Object.entries(obj)     或      obj       // 查看对象

// 1.2 查看 自身属性 和 共有属性
console.dir(obj)

// 1.3 查看共有属性(不推荐)
obj.__proto__    // 不推荐

// 1.4 判断一个属性 'xxx' 是自身的还是共有的
obj.hasOwnProperty('xxx')     // true就是自身的,false就是共有或不存在
// 'xxx' in obj 不能判断出这个属性是否是自身属性还是共有属性,可以验证是否在该对象中
// obj.hasOwnProperty('xxx') 可以判断出这个属性是否是自身属性

2. 查看某个属性

obj['key'] 或 obj.key

[四] 修改 / 增加对象的【自身】属性 (写属性)

1. 直接赋值

obj.name = 'foo'  或   obj['name'] = 'foo'

2. 批量赋值

let obj = {name: 'mou'}
Object.assign(obj, {name: 'Mia', age: 18, gender: 'female'})

3. 修改原型

let obj = Object.create(common)      // 推荐写法

[五]对象遍历

for in
Object.keys

[六] 对象检测与识别

1、typeof

typeof返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、symbol、object、undefined、function等7种数据类型,但不能判断null、array等

typeof Symbol(); // symbol 有效
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

2、instanceOf

instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

弊端

  • 对于基本数据类型来说,字面量方式创建出来的结果和实例方式创建的是有一定的区别的
console.log(1 instanceof Number)//false
console.log(new Number(1) instanceof Number)//true
  • 只要在当前实例的原型链上,我们用其检测出来的结果都是true。在类的原型继承中,我们最后检测出来的结果未必准确。
  • 不能检测null 和 undefined

3、constructor

4.Object.prototype.toString.call() Object.prototype.toString.call() 最准确最常用的方式。首先获取Object原型上的toString方法,让方法执行,让toString方法中的this指向第一个参数的值。

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window是全局对象global的引用

[七] 对象继承

7.1. 原型链(很少单独使用)

function Parent(name){
   this.name = name||'父类';
}
Parent.prototype.say = function(){
    alert(this.name);
}
function Sub(age){
   this.age = age||'21';
};
Sub.prototype = new Parent();//子类无法传递参数给超类,因为运行时已经确定了子类继承父类的信息

var oSub = new Sub('10');
oSub.say();

console.log(oSub.constructor === Parent) // true
console.log(oSub.constructor === Sub) // false

使用原型链继承主要由三个问题:

  • 字面量重写原型会中断关系(需要使用constructor修正)
  • 派生类原型引用基类实例,基类实例并非在派生类创建(new)时初始化,因此派生类无法给基类传递参数。
  • 如果重写了派生类的原型,可能导致基类属性覆盖派生类属性,所有实例共享父类实例属性,当父类引用属性被修改时,所有都会被改。
// 修正原型链后的代码:
function Parent(){
   this.name = '父类';
}
Parent.prototype.say = function(){
    alert(this.name);
}
function Sub(age){
   this.age = age||'21';
};
Sub.prototype = new Parent();// 子类无法传递参数给超类,因为运行时已经确定了子类继承父类的信息
Sub.prototype.constructor = Sub

var oSub = new Sub('10');
oSub.say();

console.log(oSub.constructor === Parent) // false
console.log(oSub.constructor === Sub) // true

7.2、类式继承(借用构造函数、对象冒充)

function Parent(name)
   this.name = name || '父类';
}

Parent.prototype.say = function(){
    alert(this.name);
}

function Sub(params){
    Parent.call(this,params);
    this.sex ='male';
};

var oSub = new Sub();
oSub.say();   // 抛出异常,实例oSub无say方法

优点

  • 每个新实例都有父类构造函数的副本
  • 每个实例都是重新实例化构造函数,不存在共享属性
  • 可以通过Parent.call(this,params)传递参数到父类构造函数

缺点

  • 只能继承构造函数的属性,无法继承父类上原型属性

7.3. 组合继承:(常用继承模式)

function Parent(name){
    this.name = name;
    this.arr = ['哥哥','妹妹','父母'];
}
Parent.prototype.say = function () {
    return this.name;
};

function Sub(name,age){
    Parent.call(this,name);//第二次调用
    this.age = age;
}
Sub.prototype = new Parent();//第一次调用

var oSub = new Sub('子类','21');
console.log(oSub);

存在问题: 超类型在使用过程中会被调用两次;一次是创建子类型的时候,另一次是在子类型构造函数的内部

7.4. 原型式继承:

function inherits(obj){
   var F = function(){};
   F.prototype = obj;
   return new F();
}
var Parent = {
    name:'父类',
    say:function(){
       alert(this.name);
    }
}
var oSub = inherits(Parent);// 不需要单独创建一个子类构造器来实例化子类。
oSub.say();

基类原型指向已有实例对象:基于已经有的实例对象创建对象,同时还不用创建自定义类型。

7.5. 寄生式继承:

function inherits(obj){
   var F = function(){};
   F.prototype = obj;
   return new F();
}
function create(o){
   var obj= inherits(o);
   obj.run = function () {
      alert('run 方法');//同样,会共享引用
   };
   return obj;
}
var Parent = {
    name:'父类',
    say:function(){
       alert(this.name);
    }
}
var oSub = create(Parent);
console.log(oSub);
oSub.say();
oSub.run();

封装创建的过程,隐藏实现细节。

7.6. 寄生组合式:(理想的继承实现方式)

function inherits(Parent,Sub){    
   var obj = Object.create(Parent.prototype);//子类原型对象
   obj.constructor = Sub;
   Sub.prototype = obj
}
function Parent(name){
    this.name = name;
    this.arr = ['哥哥','妹妹','父母'];
}
Parent.prototype.say = function () {
    return this.name;
};
function Sub(name,age){
    Parent.call(this,name);
    this.age = age;
}
inherits(Parent,Sub)

var oSub = new Sub('子类','21');
console.log(oSub);
console.log(oSub.constructor === Sub); // true

优点(解决以下几个问题):

  • 1、父类的方法可以被复用
  • 2、父类构造函数内属性不被共享
  • 3、子类构建实例时可以向父类传递参数

7.7 最终版本

const extend = function(protoProps,staticProps){
    var Super = this
    var Sub = function(){
        Super.apply(this,arguments)
    }
    if(protoProps && protoProps.hasOwnProperty('constructor') && typeof protoProps.constructor === 'function'){
        Sub = protoProps.constructor
    }
    // 1、继承父类原型链上,其属性不被共享
    var parentProps = Object.create(Super.prototype)
    parentProps.constructor = Sub
    // 2、继承静态方法
    Object.assign(Sub,staticProps)
    // 3、切断原型链
    Sub.prototype = Object.assign(parentProps,protoProps)
    return Sub
}

写个实例验证下:

var Parent = function (name){
    this.name = name
}
Parent.prototype.say =  function(){ alert(this.name)}
Parent.extend = extend // 静态防范挂载上
var Sub = Parent.extend({
    constructor:function Sub(name,age){
        Parent.call(this,name)
        this.age = age
    },
    sayHello(){
        alert("hi",this.name)
    }
},{
   verision:"1.0"
})
var oSub = new Sub("Tim",21)
oSub.say()
console.log(Sub.version)