JS原型/继承(原型链)透彻学明白

40 篇文章 1 订阅
39 篇文章 5 订阅

目录

一、原型继承

1.原型:[[Prototype]]

 2.写入不使用原型

3. “this” 的值

4.for  ... in 循环(该循环也会迭代继承的属性)

二、F.protoytype

1.

 2.默认的 F.protoytype,构造器属性

三、原生的原型

1.Object.prototype

2.其他内建原型

2.更改原生类型

四、原型方法,没有 _proto_对象


​​​​​​​

一、原型继承

1.原型:[[Prototype]]

        当我们从object中 读取一个缺失的属性时,JavaScript会自动从原型中获取该属性,我们称之为“原型继承”。

        属性  [[Prototype]] 是内部并且隐藏的,有很多设置它的方法,这里先使用 __proto__。

let animal = { eats: true };

let rabbit = { jumps: true };

rabbit.__proto__ = animal; // (*)

// 现在这两个属性我们都能在 rabbit 中找到:

alert( rabbit.eats ); // true (**)

alert( rabbit.jumps ); // true

这里的(*)行将animal 设置为 rabbit 的原型

当alert 试图读取 rabbit.eats(**)时,因为它不存在于rabbit 中,所以JavaScript 会顺着 [[Prototype]] 引用,在animal 中 自下而上 查找:

 在这里我们可以说 “animal 是rabbit 的原型”,或者说 “rabbit 的原型是从 animal 继承而来的”。

因此,如果 animal 有许多有用的属性方法,将自动地变为在为rabbit 中可用。这种属性被称为“继承”。

当然,也能从原型中获得方法,原型链也可以很长。

let animal = {

        eats: true,

        walk() {

                alert("Animal walk");

        }

};

let rabbit = {

        jumps: true,

        __proto__: animal

};

let longEar = {

        earLength: 10,

        __proto__: rabbit

};

// walk 是通过原型链获得的

longEar.walk(); // Animal walk

alert(longEar.jumps); // true(从 rabbit)

        现在,如果我们从 longEar 中读取一些它不存在的内容,JavaScript 会先在 rabbit 中查找,然后在 animal 中查找。

这里有两个限制:

1.引用不能形成闭环。如何我们试图在一个闭环中分配 __proto__ ,JavaScript会抛出错误。

2. __proto__的值可以是对象,也可以是null 。而其他的类型都会被忽略。

!!这里需要强调的是,只能有一个  [[Prototype]] 。一个对象不能从其他两个对象获得继承。

__proto__ 和 [[Prototype]] 区别

1)__proto__ 是 [[Prototype]] 的 getter/setter 

2)现在更多会使用 Object.getPrototypeof/Object.setPrototypeof 来取代 __proto_ 去 get/set 原型

3)为了标记性更强,接下来的示例中,我们使用 __proto__

 2.写入不使用原型

  • 原型仅用于读取属性
  • 对于写入 / 删除 操作 可以直接在 对象上进行
  • 如果属性/方法能够在对象中找到,则无需使用原型

分配操作是由setter 函数处理的。因此,写入此类属性实际上与调用函数相同。

所以,下面这段代码中的 admin.fullName 能够正常运行。

let user = {

        name: "John",

        surname: "Smith",

        set fullName(value) {

                [this.name, this.surname] = value.split(" ");

        },

        get fullName() {

                return `${this.name} ${this.surname}`;

        }

};

let admin = {

        __proto__: user,

        isAdmin: true

};

alert(admin.fullName); // John Smith (*)

// setter triggers!

admin.fullName = "Alice Cooper"; // (**)

alert(admin.fullName); // Alice Cooper,admin 的内容被修改了

alert(user.fullName); // John Smith,user 的内容被保护了

在(*)行中,属性 admin.fullName 在原型 user 中有一个 getter,因此它会被调用。

在(**)行中,属性在原型中有一个 setter,因此它会被调用。

3. “this” 的值

        在上面例子中,可能会发现 在 set fullName(value) 中的 this 的值是什么? 属性 this.name 和 this.surname 被写在哪里 :在user 还是admin ?

        答案很简单: this 根本不受原型的影响。

        无论在哪里找到的方法:在一个对象还是原型中。在一个方法调用中,this 始终是 点符号 . 前面的对象

           所以,setter 调用admin.fullName= 使用 admin 作为this ,而不是 user。

  上面两段话如果没理解请多读两遍

           我们很有可能会有一个带有很多方法的大对象,并且还有从其继承的对象。当继承的对象运行继承的方法时,他们仅修改自己的状态,而不会修改大对象的状态。

                如下所例:这里的animal 代表 “方法存储”,rabbit 在使用其中的方法。

                调用 rabbit.sleep( ) 会在 rabbit 对象上设置 this.isSleeping:

// animal 有一些方法

let animal = {

        walk() {

        if (!this.isSleeping) {

                alert(`I walk`);

        }

},

        sleep() {

                this.isSleeping = true;

        }

};

let rabbit = {

        name: "White Rabbit",

        __proto__: animal };

// 修改 rabbit.isSleeping

rabbit.sleep();

alert(rabbit.isSleeping); // true

alert(animal.isSleeping); // undefined(原型中没有此属性)

       

        

        

如果我们还有从 animal 继承的其他对象,像 bird 和 snake 等,它们也将可以访问 animal 的方法。但是,每个方法调用中的 this 都是在调用时(点符号前)评估的对应的对象,而不是 animal。因此,当我们将数据写入 this 时,会将其存储到这些对象中。

所以,方法是共享的,但对象状态不是。

4.for  ... in 循环(该循环也会迭代继承的属性)

 let animal = { eats: true };

let rabbit = {

jumps: true,

__proto__: animal

};

// Object.keys 只返回自己的 key

alert(Object.keys(rabbit)); // jumps

// for..in 会遍历自己以及继承的键

for(let prop in rabbit) alert(prop); // jumps,然后是 eats

注意:Object.keys 只返回自己的 的对象属性,不会返回原型链的。

         但是 for...in 会遍历自己以及继承的键

如果上面不能实现我们想要的,并且想排除继承的属性。那么这有一个方法:

obj.hasOwnProperty(key):  如果 obj 具有自己的(非继承性)名为 key 的属性,则返回true。

let animal = {

        eats: true

};

let rabbit = {

        jumps: true,

        __proto__: animal

};

for(let prop in rabbit) {

        let isOwn = rabbit.hasOwnProperty(prop);

        if (isOwn) {

                alert(`Our: ${prop}`); // Our: jumps

        } else { alert(`Inherited: ${prop}`); // Inherited: eats

        }

}

这里我们有以下继承链:rabbit 从 animal 中继承,animal 从 Object.prototype 中继承(因为 animal 是对象字面量 {...},所以这是默认的继承),然后再向上是 null 

 注意:几乎所有的其他键/值获取方法 都忽略继承的属性

例如:Object.keys 和 Object.values 等,都会忽略继承的属性。它们只会对对象自身进行操作。不考虑 继承自原型的属性。

二、F.protoytype

1.

这里的F.protoytype 指的是 F 的 一个名为“”protoytype“的常规属性。下面例子

let animal = {

        eats: true

};

function Rabbit(name) {

        this.name = name;

}

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal

alert( rabbit.eats ); // true

设置 Rabbit.prototype = animal;  的字面意思是:当创建了一个 new Rabbit时,把它的 [[Prototype]]  赋值为 “animal”

 F.protoytype 仅用在 new F

F.protoytype   属性仅在 new F   被调用时使用,它为新对象的 [[Prototype]] 赋值 

如果在创建之后, F.protoytype  属性有了变化 (F.prototype = <another object>),那么通过 new F  创建的新对象也随之拥有新的对象作为 [[Prototype]] ,但已经存在的对象将保持旧的值。

 2.默认的 F.protoytype,构造器属性

每个函数都有  “protoytype”属性,即使我们没有提供它。默认的 “protoytype” 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。

像这样,并检查一下

function Rabbit() {}

/* default prototype

Rabbit.prototype = { constructor: Rabbit };

*/

alert( Rabbit.prototype.constructor == Rabbit ); // true

 通常如果我们什么都不做, constructor 属性 可以通过 [[Prototype]] 给所有 rabbits 使用:

function Rabbit() {}

/* default prototype

Rabbit.prototype = { constructor: Rabbit };

*/

let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}

alert( rabbit.prototype.constructor == Rabbit ); // true (from prototype)

重要 !!

关于 constructor 最重要的是,constructor 存在于函数默认“prototype”中,如果我们将整个默认prototype 替换掉,那么其中就不会有 “constructor” 了。

比如:

​​​​​​​function Rabbit() { }

Rabbit.prototype = {

        jumps: true

};

let rabbit = new Rabbit();

alert(rabbit.constructor === Rabbit); // false

因此为了确保正确的“constructor”,我们可以选择添加/删除属性到默认“prototype”,而不是将其整个覆盖:

function Rabbit() {}

// 不要将 Rabbit.prototype 整个覆盖

// 可以向其中添加内容

Rabbit.prototype.jumps = true

// 默认的 Rabbit.prototype.constructor 被保留了下来

或者,也可以手动重新创建 constructor 属性:

Rabbit.prototype = {

        jumps: true,

        constructor: Rabbit

};

// 这样的 constructor 也是正确的,因为我们手动添加了它

注意:

  • F.prototype 的值要么是一个对象,要么就是 null :其他值都不起作用
  • “prototype”属性仅在设置了一个构造函数,并通过 new 调用时,才具有这种特殊的影响。
  • 默认情况下,所有函数都有 F.prototype ={ construct: F },所以我们可以通过访问它的 “constructor” 属性来获取一个对象的构造器。

三、原生的原型

1.Object.prototype

先输出一个空对象

let obj = {};

alert( obj ); // "[object Object]" ?

简短的表达式 obj = {} 和 obj = new Object() 是一个意思。其中Object就是一个内建的对象构造函数,其自身的恶prototype 指向一个 带有 toString 和其他方法的一个巨大对象。

当new Object()被调用 (或一个字面量对象 {...}被创建),按照上面内容 F.protoytype ,这个对象的 [[Prototype]] 属性被设为  Object.prototype

 

所以,之后当obj.toString() 被调用时,这个方法是从 Object.prototype 中获取的。

可以用如下验证

let obj = {};

alert(obj.__proto__ === Object.prototype); // true

alert(obj.toString === obj.__proto__.toString); //true

alert(obj.toString === Object.prototype.toString); //true 

注意在 Object.prototype 上方链中没有更多的  [[Prototype]]

alert(Object.prototype.__proto__); // null

2.其他内建原型

其他内建对象,像 Array、Date、Function 及其他,都在prototype 上挂载了方法。

按照规范,所有的内建原型顶端都是  Object.prototype。这就是为什么“一切都从对象继承而来”。

接下来手动验证原型:

let arr = [1, 2, 3];

// 它继承自 Array.prototype?

alert( arr.__proto__ === Array.prototype ); // true

// 接下来继承自 Object.prototype?

alert( arr.__proto__.__proto__ === Object.prototype ); // true

// 原型链的顶端为 null。

alert( arr.__proto__.__proto__.__proto__ ); // null

//可以验证一下一切都从对象继承而来这句话

alert (typeof(arr.__proto__)).   //object

!!!特殊值 null 和 undefined 比较特殊。他们没有对象包装器,所以他们没有方法和属性,并且他们也没有相应的原型。

2.更改原生类型

内建原型可以被修改或被用新的方法填充。但是不建议更改他们,唯一允许的情况可能是,当我们添加一个还没有被JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准,才可能允许这样做。

四、原型方法,没有 _proto_对象

之前我们说过,__proto__ 是被认为过时且不推荐使用的,因为proto 必须仅在浏览器环境下才能得到支持。

现在我们用到的方法如下来代替  _proto_

Object.create(proto,[descriptors])--  利用给定的 proto  作为 [[Prototype]] 和可选 的属性描述来创建一个空对象​​​​​​​

​​​​​​​​​​​​​​​​​​​​​Object.getPrototypeOf(obj) ---  返回对象 obj 的 [[Prototype]]。

Object.setPrototypeOf(obj,proto) -- 将对象 obj 的 [[Prototype]] 设为 proto

例如:

let animal = { eats: true }; // 创建一个以 animal 为原型的新对象

let rabbit = Object.create(animal);

alert(rabbit.eats); // true

alert(Object.getPrototypeOf(rabbit) === animal); // true Object.setPrototypeOf(rabbit, {}); // 将 rabbit 的原型修改为 {}

我们也可以使用 Object.create 来实现比复制 for..in 循环中的属性更强大的对象克隆方式:

let clone = Object.create(Object.getPrototypeOf(obj),    Object.getOwnPropertyDescriptors(obj));

此调用可以对 obj 进行真正准确的拷贝,包括所有的属性:可枚举和不可枚举的,数据属性和。setters /getters -----包括所有内容,并带有正确的  [[Prototype]]

其他方法:

所有返回对象属性的方法,(如 Object.keys 及其他)---都返回 “自身”。如果我们想继承他们,我们可以使用 for ... in

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值