js基础系列之【原型和原型链】

声明:形成本文的出发点仅仅是个人总结记录,避免遗忘,并非详实的教程;文中引用了经过个人加工的其它作者的内容,并非原创。学海无涯

引入问题

  一般我们是这样写的:

  (需求驱动技术,疑问驱动进步)

// 构造函数
function Obj(name) {
    this.name = name;
}

// 原型
Obj.prototype.sayName = function() {
    return `say: ${this.name}`;
}

// '实例'
var obj = new Obj('a');
// ’实例'的属性和方法
obj.name // 'a'
obj.sayName() // 'say: a'

我们都知道实例obj的属性’name‘【是实例自己的】,而实例的sayName方法是【所有实例共享的(?)】是从原型中查找的(?)

如此就引出一些问题:

  1、在实例中没有定义的方法在原型中是怎么查找的?

  2、实例中共享的方法如果在原型中修改了,会不会影响所有实例? 如果在某一个实例中重新定义了该方法会不会对其它实例造成影响?

  问题2我们可以先测试一下,看看结果

  

// 创建另一个实例
var obj_b = new Obj('b');

// 在实例中修改从原型中查找到的共享方法sayName
obj.sayName = 'changed';

obj.sayName // 'changed'
obj_b.sayName // function() {...}

// 直接修改原型中的sayName方法
Obj.prototype.sayName = 'change from proto';

obj.sayName // 'changed'
obj_b.sayName // 'change from proto'

总结: 1、修改实例中从原型中’继承‘(暂时用这个词)的属性和方法是不会影响到其它实例的

    2、修改原型中的方法会立即反应到所有实例中(如果一个实例重新定义了自己的该同名方法,则不会);

结论有了,但为什么呢? next -->

对象‘特殊’的内置属性[[prototype]]

先说结论

  1、javascript中的对象有一个特殊的内置属性[[prototype]],这个内置属性表示和其它对象之间的关联(我们通俗点可以说成:对其它对象的引用);

  2、几乎所有的对象在创建时[[prototype]]都会指向一个非空的对象(当然可以是空对象,而且这种对象很有用处)

  3、当试图访问对象的一个属性时,会触发[[get]] 操作,默认的[[get]]操作会先检查对象本身是否有这个属性,如果有,则返回,如果没有就需要使用[[prototype]]进行查找了,如果在[[prototype]]中找到该属性,也同样会触发[[get]]操作

  4、我们可能已经猜到了,和结论3类似,当试图对对象的一个属性重新赋值时,同样会现查找该属性值,如果本身有则设置本身的该属性,如果没有则沿着[[prototype]]去查找

  5、[[prototype]]的尽头在哪? 答案是所有普通的[[prototype]]最终都会指向Object.prototype(js内置的对象, Object是一个内置函数 当然函数也是对象-_-)

  OK,上代码:

  

var obj_pro = {
    _pri_: 'e', // 模拟私有属性
    a: 'a',
    func() {},
};
Object.defineProperty(obj_pro, 'b', {
    writable: false,
    enumable: true,
    configurable: true,
    value: 'b',
});
Object.defineProperty(obj_pro, 'c', {
    writable: true,
    enumable: true,
    configurable: false,
    value: 'c',
});
Object.defineProperty(obj_pro, 'd', {
    writable: true,
    enumable: false,
    configurable: true,
    value: 'd',
});
Object.defineProperty(obj_pro, 'e', {
    get() {
        return this._pri_ + 'get';
    },
    set(val) {
        this._pri_ = val + 'set';
    },
});
var obj_son = Object.create(obj_pro); // Object.create的作用通俗点就是使obj_son的[[prototype]]指向obj_pro
obj_son.own = 'own';

// 读取属性
obj_son.own // 'own'
obj_son.a // 'a'
obj_son.b // 'b'
obj_son.c // 'c'
obj_son.d // 'd'
obj_son.e // '1get' 执行了[[get]]操作
obj_son.func // func() {}

// 设置属性
obj_son.own = 'set own'; // 设置完成后: 'set own'
obj_son.a = 'set a'; // 设置完成后: 'set a'
obj_son.b = 'set b'; // 设置完成后: 'b'
obj_son.c = 'set c'; // 设置完成后: 'set c'
obj_son.d = 'set d'; // 设置完成后: 'set d'
obj_son.e = 'set e'; // 设置完成后: '1setget'

代码看起来有点多... 其实只是我们创建了很多属性而已。 

经过上面代码中的一通赋值操作,obj_son自身的属性有: own、a、c、d、_pri_(这个是因为我们在设置e属性的时候执行了[[set]]操作,而[[set]]操作中的this执行了obj_son)

理一理: 两个对象: obj_pro  obj_son 通过Object.create将obj_son 的[[prototype]] 指向了obj_pro,这样两个对象之间就有了内在的关联

     我们在obj_pro中创建了a、b、c、d、e 5个属性; a是默认类型属性,b、c、d通过Object.defineProperty去定义了 三个属性特性,e是我们定义的存取器      ([[getter]]/[[setter]])obj_son.own是我们后来定义在它自身上面的属性,用于做一个区分

     可以看到我们在对象obj_son上去重新赋值属性b和属性e时结果是不成功的,为什么呢?

有一个结论about属性设置和屏蔽

  当我们进行类似 myObj.foo = 'bar'这样的属性设置过程是这样的, 如果对象myObj自身包含名为‘foo’的属性,则修改已有属性,如果myObj自身和[[prototype]]中都存在‘foo’属性,则[[prototype]]中的属性就会被忽略;如果没有[[prototype]]就会被遍历;如果在[[prototype]]链中也没有‘foo’属性,则会在myObj对象自身上创建属性‘foo’并赋值;如果[[prototype]]中查找到‘foo’,则会有下面几种情况,而且有一些我们想不到的情况

      1. 如果在[[Prototype]] 链上层存在名为foo 的普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在myObject 中添加一个名为foo 的新属性,它是屏蔽属性。

      2. 如果在[[Prototype]] 链上层存在foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。

      3. 如果在[[Prototype]] 链上层存在foo 并且它是一个setter,那就一定会调用这个setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo 这个setter。

是不是很乱,很烦,哈哈这就是js(逃);

其实到现在我们所涉及的原型是很简单的,图:

         图 1

结合这个简单的图我们上述总结的就是对象在属性读取和属性设置的时候发生的和其相关的对象之间的一些过程;

通过js中的‘类’来了解原型链

一般我们在日常开发中总是听到面向对象开发模式这样的词汇,那js中的‘面向对象’是什么样子的呢?

构造函数创建对象

// 构造函数
function Animal(weight) {
    this.weight = weight;
}

Animal.prototype.eat = function() {
    console.log('eat');
}

// 实例
var anim1 = new Animal(10);
anim1.weight // 10
anim1.eat // console -> 'eat'

    似乎是回到了我们之前引入的问题当中了,但是经过有关[[prototype]]的分析,我们很容易就会形成这样一幅图:

          图 2

实际上构造函数的唯一用途就是构建和初始化实例对象

构造函数在js中并不是一种特殊的函数,只有使用 new 操作符调用的函数才是构造函数,本质上构造函数和普通函数没什么区别

每一个函数在创建时都会有一个prototype属性指向一个对象,该对象默认有一个constructor属性指向该函数,这个对象就称为该函数的原型对象

这里需要了解的是 使用new 操作符调用函数时究竟发生了什么

'var instance = new Func()'

过程是:首先创建一个对象;将函数调用的this绑定至这个对象;将该对象的[[prototype]]指向Func的原型对象即: Func.prototype;执行Func函数的代码,如果函数Func没有显式的return一个对象(return;或者return非对象同)就返回最初创建的对象;到这里就是我们熟知的构造函数创建对象的过程;

如果函数Func显式得返回了一个对象(如果显式return this同之前的过程),那么之前创建的对象就会被丢弃,返回显式返回的对象;显式返回的对象的[[prototype]]就不会指向Func.prototype对象,所以永远也不要使用new 去调用这样的函数,因为会引起歧义;

以上我们就解释了 图 2所述的关系;

[[prototype]] 和 __proto__

我们总是说[[prototype]],那我们有没有办法去验证这个关系呢? 其实在某些浏览器中(例如chrome)中提供了一个__proto__属性作用基本等同于[[prototype]],这个属性并没有出现在标准中,所以,永远也不要在项目中使用

验证我们的观点:

anim1.__proto__ === Animal.prototype // true

更进一步,构造函数Animal是个函数当然也是对象,按照我们之前说的,每个对象在创建的时候都会将[[prototype]]指向另一个对象,Animal的[[prototype]](__proto__)指向哪里呢? 测试一下:

Animal.__proto__ === Function.prototype  // true
Function.prototype.__proto__ === Object.prototype  // true
Object.prototype.__proto__ // null

Animal.prototype.__proto__ === Object.prototype  // true

Object.__proto__ === Function.prototype  // true

是不是有点乱? 看来是时候更新完善我们的图了:

个人观点: 我们姑且把对象和函数先分开看待,prototype是函数的属性;[[prototype]]/__proto__是对象和对象之间关系的纽带;constructor是对象的一个属性,与prototype指向相反(当然其实是不可靠的,实际开发中最好不要使用这个属性)

沿着蓝色线条我们可以清楚地看出一个原型组成的链条,这就是我们所说的原型链

合理推测:我们所创建的函数(包括所谓的构造函数)是通过Function构造函数创建的,创建的函数func的[[prototype]]/__proto__指向Function.prototype;

     创建的所有对象都是通过Object构造函数创建的,创建的对象obj的[[prototype]]/__proto__指向Object.prototype(当然包括Function.prototype);

最终所有的对象(包括函数)的[[prototype]]链条最末端都指向了Object.prototype,而Object.prototype的[[prototype]]/__proto__最终都归向null (这是不是暗示我们万物终归尘土,代码敲到最后就是不敲代码, 哈哈哈哈开个玩笑)

来点复杂的

// 构造函数
function Animal(weight) {
    this.weight = weight;
}

Animal.prototype.eat = function() {
    console.log('eat');
};

function Dog(name) {
    this.name = name;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.bark = function() {
    console.log('wang wang');
};

var doggie = new Dog('da huang');

通过上图我们会发现Dog.prototype自身并没有constructor,这是不是漏掉了?测试一下

doggie.constructor // Animal
Dog.prototype.hasOwnProperty('constructor') // false

是的,并不是我们想象的那样,实例doggie的constructor并不是Dog,而是沿着原型链一直找到了 Animal.prototype.constructor,是因为我们重写了Dog.prototype!!!

所以通过constructor去查找构造函数的方法并不那么可靠,但是我们为什么要知道构造函数呢???

所以我们为了面向对象而创建的构造函数,通过构造函数创建实例(对象),模仿传统面向对象语言的继承,结果呢? 我们得到的其实还是一堆对象,构造函数在创建完对象之后好像并没有什么实际用途了。

原文地址:https://www.cnblogs.com/innooo/p/10078418.html