6

对象的创建

在JavaScript中创建一个对象有三种方式。可以通过对象直接量关键字newObject.create()函数来创建对象。

1. 对象直接量

创建对象最直接的方式就是在JavaScript代码中使用对象直接量。在ES5中对象直接量是由若干 名/值组成的映射表, 整个映射表由{}包含起来。每个名/值中间使用:进行分割,名/值之间使用,进行分割。

var o1 = {};
var o2 = {name: 'javascript'}
var o3 = {title: 'object', o2: o2} 
// 数组、日期、函数、正则等作为特殊的对象,这里暂不讨论
// ES6 也暂时不讨论

上面就是使用对象直接量创建对象,这种方式比较简单方便。

2. 通过new创建对象

通过关键字new + 函数调用,就可以创建一个新的对象。被调用的函数被称为构造函数。 根据高程中描述,使用 new + 调用函数 创建一个对象,这种方式会经历以下 4 个步骤:

(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。

举个例子:

    var Foo = function(name) {
         this.name = name
    }
    
    var foo = new Foo('我'); 
    /*
        var foo = new Foo('我') 
        创建对象大致的流程是
        (1)  var obj = {};
        `高程`中步骤(2),(3)如果不清楚的小伙伴,可以参考下面的实现
        (2)、(3) Foo.call(obj); call 方法能够改变函数Foo的执行上下,把this指向obj,然后执行Foo函数
        (4) foo = obj;   
    */
    typeof foo; // "object"
    foo.constructor === Foo // true  
3. 通过Object.create() 创建对象

这里是ES5官方提供的一个创建对象的方法。

    var obj = {name: 'javascript'};
    var newObj = Object.create(obj);
    newObj.name // => javascript

原型

JS中每个函数都可以看成一个对象,而原型(prototype)就是函数中的其中一个属性。这里要很清楚,原型是函数上面的一个属性,这个属性只有函数对象才能拥有,别的类型是没有prototype属性。而原型的作用就是它所引用的对象能够被拥有它的函数所构建的实例化对象所访问。

那么原型是怎么和对象建立联系的?

编写代码如下:

let obj = {name: 'javascript'};
console.log(obj.name) // => javascript
console.log(obj)

控制台输出如下:

clipboard.png

我们在程序中定义一个JavaScript对象,然后打印这个对象,这里除了前面定义的name属性外,还有另外一个__proto__属性。前面说道 函数 上面的 prototype(原型)所指向的对象能够拥有它的函数所构建的实例化对象所访问。至于具体怎么访问的细节没有说明。其实就是通过__proto__这个属性作为桥梁进行的联接。

let obj = {name: 'javascript'};
console.log(obj.__proto__ === Object.prototype) //true

对比发现__proto__所指的对象和Object.prototype所指的是一样的。我们是可以认定__proto__就是这座桥梁,那么obj就能访问到Object.prototype所指的对象就是理所当然了。

于是我就在猜测在使用直接定义量去定义对象的时候,在底层的实现很有可能就是通过new Object()的这种方式实现的。

于是我编写了下面的测试代码:

 let obj = {name: 'javascript'};
 let obj1 = new Object({name: 'javascript'});

发现上面的obj与obj1两者数据结构基本一致。

原型对象

原型对象简单来说就是函数的原型所指向的对象。前面说原型的时候,说了Object.prototype所指对象就是Object(函数)的原型对象。 在每个函数的原型对象中,默认会有constructor属性,用于指向函数本身。

Object.prototype.constructor === Object // true
let Test = function() {console.log('test')};
Test.prototype.constructor === Test // true

在最开始的时候,原型对象的constructor设计主要是为了获取对象的构造函数。后来发现constructor属性易变,不可信。推荐使用instanceof。

var Test = function() {console.log('test')};
var test = new Test();
console.log(test.constructor); // Test
test.constructor = Object;
console.log(test.constructor); // Object
/*这里想使用 test.constructor 来判断是否是Test的实例化对象就不可信。而应该使用 instanceof */
test instanceof Test // true

原型对象有什么作用,主要实现对象的继承。

例如我们常用的对象、数组、函数都是得益于原型。

当我们使用变量直接量定义一个对象的时候,其实我们是没有定义它上面的这些能够调用的方法

let obj = {};

clipboard.png
这些方法怎么来的,就是通过调用Object上面的原型对象而来的。

console.log(Object.prototype)

clipboard.png
同理数组(通过调用Array.prototype),函数(通过调用Function.prototype)

原型链

ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原 型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的 原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数 的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实 例与原型的链条。这就是所谓原型链的基本概念。

继承

在没有使用ES6的 extends 实现继承以前,我们可以通过对ES5的原型特性来实现继承。
对于JS的继承来说,我认为就是解决原型链的排列问题。
在一个基础的构造函数new 一个对象实例的时候,它的原型链就已经生成好了

function F() {
}
let f = new F()

image.png
上面的图中,我们能够很清楚的看到,从实例往上寻找,总共能够找到两个__proto__。第一个是函数本身的原型对象。 这个是在函数声明的时候就有,我们可以通过 F.prototype 查看到。那么这个对象是由那个函数生成的呢。 通过上面的图片,我们能够很清晰的看清楚,就是Object。

我们可以这样去归纳:在一个函数声明的时候,会通过Object会生成一个对象,这个对象里面会有一个属性 constructor指向函数本身。 这个对象被作为原型对象赋值到函数的prototype上面。

回归到我们的标题本身,那继承呢。
在一个函数生成的实例中,它的原型链就只有两层,函数本身以及生成函数原型对象的Object。如果它想复用一些已有的函数方法与属性。 怎么弄呢,那就在函数和Object中添加一层对象就可以咯

const Parent = {
    name: '父亲',
    sayName () {
        console.log(this.name)
    }
}

function Child (value) {
    this.value = value
}

Child.prototype = {...Parent, constructor: Child}
let child = new Child(10)
let child1 = new Child(11)
console.log(child, child1)

说明: 直接定义的对象也是有Object生成的,所以默认是继承Object.prototype

在Child与Object两个原型之间,我们新添加一个新的对象Parent。这样我们在Child生成的实例就能够复用Parent中实例与方法。

但是这个是有问题的,当存在多个实例的时候,一个实例修改了原型属性,另一个实例的原型属性也会被修改。我们需要把属性放到实例本身,而不是原型上面。

我们需要把上面的Parent变为函数,把属性的放到函数中,方法放到函数的原型对象中 Parent.prototype。可以想象Parent就变成了定义公共属性的一个函数,而Parent.prototype就是我们前面只有方法的Parent对象。

function Parent (name) {
   this.name = name
}

// 这里实现了 属性的复用, 不用重复定义相同属性
function Child (value, name) {
    Parent.call(this, name)
    this.value = value
}

通过使用函数的方式,能够让我们子类不用定义重复的属性,但是除了属性外, 我们还需要处理共有的方法。
在这里我能够想到的理想情况是这样的。子类的原型对象正好是父类的实例。这样会有如下好处,子类的原型对象上面可以任意添加方法, 而不会影响其他子类,父类上面添加的方法对所有继承它的子类都有效。于是我们就可以这样

    Child.prototype = new Parent()

但是上面还是会有问题的, new Parent()会生成一个实例,这个实例里面有包含构造函数生成的属性。这些属性对于我们来说,是不需要的。 我们需要的是一个纯的对象,纯到只有一个原型,并且这个原型指向Parent.prototype.

    // 然后我们就生成这样一个对象
    const F = function() {}
    F.prototype = Parent.prootype
    let pureObject = new F()

image.png
这个就是我们先要的对象,只要把这个对象赋给 Child.prototype,然后添加一个构造函数,就完成了

    // 然后我们就生成这样一个对象
    const F = function() {}
    F.prototype = Parent.prootype
    let pureObject = new F()
    
    Child.prototype = pureObject
    Child.prototype.constructor = Child

顺便我们还可以看看 ES6的实现

function Parent (name) {
       this.name = name
     }

 Parent.prototype.sayName = function () {
   console.log(this.name)
 }

 class Child extends  Parent {
   constructor(name, value) {
     super(name)
     this.value = value
   }
 }

 console.log(new Child('小明', 12))

image.png
是不是很熟悉呀!!!


火星田园犬
933 声望685 粉丝

小心驾驶, 专业埋雷