秋雨
首页
  • HTML
  • CSS
  • JavaScript
设计模式
webpack
  • 前端常识
  • ES
  • React
  • Redux
  • ReactRouter
  • 事件循环
  • 浏览器渲染流程
  • Vue项目性能优化
  • Vue
  • App
Github
首页
  • HTML
  • CSS
  • JavaScript
设计模式
webpack
  • 前端常识
  • ES
  • React
  • Redux
  • ReactRouter
  • 事件循环
  • 浏览器渲染流程
  • Vue项目性能优化
  • Vue
  • App
Github
  • JavaScript基础
  • Object(基础)
  • 数据类型
  • 函数进阶
  • 对象属性配置
  • 原型、继承
  • class
  • 错误处理
  • Promise

原型、继承

[[Prototype]]

在 JavaScript 中,对象有一个特殊的隐藏属性[[Prototype]],它要么是null,要么是对另一个对象的引用。该对象被称为原型。

alt text

当我们从一个object中读取一个缺失的属性时,javascript 会自动从原型中获取该属性。这种方式被称为“原型继承”。

读取或设置原型:__proto__

let animal = {
  eats: true,
};
let rabbit = {
  jumps: true,
};

rabbit.__proto__ = animal;

当自身对象没有访问的某个属性时,会访问原型对象上的属性。

let animal = {
  eats: true,
};
let rabbit = {
  jumps: true,
};

rabbit.__proto__ = animal; // (*)

// 现在这两个属性我们都能在 rabbit 中找到:
alert(rabbit.eats); // true (**)
alert(rabbit.jumps); // true

alt text

对象会自动继承原型链上的方法和属性。

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  },
};

let rabbit = {
  jumps: true,
  __proto__: animal,
};

// walk 方法是从原型中获得的
rabbit.walk(); // Animal walk

多个原型的继承被称为原型链,它们有两个限制:

  • 不能形成闭环
  • __proto__的值可以是对象,也可以是null。其他类型会被忽略

__proto__属性是[[Prototype]]因历史原因留下的 getter/setter。

在现代模式下,建议使用Object.getPrototypeOf和Object.setPrototypeOf来获取和设置[[Prototype]]。

写入不使用原型

原型仅用于读取属性。

对于写入/删除操作可以直接在对象上进行。

let animal = {
  eats: true,
  walk() {
    /* rabbit 不会使用此方法 */
  },
};

let rabbit = {
  __proto__: animal,
};

rabbit.walk = function () {
  alert("Rabbit! Bounce-bounce!");
};

rabbit.walk(); // Rabbit! Bounce-bounce!

在访问器上有一个例外,

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 的内容被保护了

this 的值

this的值取决于函数如何被调用。不受原型的影响。

for...in 循环

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

如果想排出继承的属性可以使用 obj.hasOwnProperty(key)

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
  }
}

为什么Object.hasOwnProperty没有出现在循环中。

因为Object.hasOwnProperty的枚举属性被设置成了false, enumerable:false。

F.prototype

F.prototype属性在创建对象时被设置成[[Prototype]]。

这是function的常规属性。它不是[[Prototype]],但它指向一个对象,该对象被设置为[[Prototype]]。

let animal = {
  eats:true
}

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

//当创建一个`new Rabbit`时,把它的[[Prototype]]赋值为animal
Rabbit.prototype = animal;

let rabbit = new Rabbit('White Rabbit');

console.log(rabbit.eats); // true

如下图所示:

alt text

prototype是一个水平箭头,表示一个常规属性,[[Prototype]]是一个垂直箭头,表示rabbit继承自animal。

F。prototype 仅用在new F时

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

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

构造器(constructor)

默认的prototype是一个只有属性constructor的对象,属性constructor指向函数自身。

function Rabbit() {}

/* 默认的 prototype
Rabbit.prototype = { constructor: Rabbit };
*/

alt text

测试一下:

function Rabbit(name) {
  
}

console.log(Rabbit.prototype.constructor === Rabbit); // true

constructor指向的是构造函数。

function Rabbit(name) {
  
}
let rabbit = new Rabbit('White Rabbit');

console.log(rabbit.constructor === Rabbit); // true

alt text

我们可以使用constructor属性来创建一个新对象,该对象使用与现有对象相同的构造器。

function Rabbit(name) {
  this.name = name;
  console.log(name);
}

let rabbit = new Rabbit('White Rabbit');
let rabbit2 = new rabbit.constructor('Black Rabbit');

但是 construtor是可以被覆盖丢失的。

比如:

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

Rabbit.prototype = {
  jumps:true
}
let rabbit = new Rabbit('White Rabbit');
console.log(rabbit.constructor === Rabbit); // false
console.log(rabbit.constructor === Object)//true

当Rabbit.prototype被覆盖时,constructor属性也被丢失了。这时它指向的是Object。

如果不想丢失constructor属性,可以手动添加。

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

Rabbit.prototype = {
  jumps:true,
  constructor:Rabbit
}
// or 

Rabbit.prototype.jumps = true;
let rabbit = new Rabbit('White Rabbit');
console.log(rabbit.constructor === Rabbit); // true

总结

  • F.prototype属性(不要把它和[[Prototype]]弄混了)它只有在new F时才会被新对象使用,给新对象的[[Prototype]]赋值。
  • F.prototype的值要么是一个对象,要么就是null:其他值都不起作用。
  • prototype属性仅当设置在一个构造函数上,并通过new调用时,才具有这种特殊的影响。

在常规对象上,prototype属性没有任何特殊的作用。

let user = {
  name:'John',
  prototype:'bla-bal',//这里只是普通对象
}

默认情况下,所有函数都有F.prototype = {constructor:F},所以我们可以访问constructor属性来获取一个对象的构造器。

原生的原型

Object.prototype

const obj = {};
alert(obj) '[object Object]'

生成字符串'[object Object]'的代码就是内建的toString方法,它催在与Object.prototype上。

alt text

当 new Object()被调用(或一个字面量对象{...}被创建),这个对象的[[Prototype]]被设置为Object.prototype。

alt text

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

验证一下:

let obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(obj.toString === obj.__proto__.toString); // true

console.log(obj.toString === Object.prototype.toString); // true

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

console.log(Object.prototype.__proto__); // null

其他内建原型

alt text

let arr = [1,2,3];
console.log(arr.__proto__ === Array.prototype); // true

console.log(arr.__proto__.__proto__ === Object.prototype); // true

console.log(arr.__proto__.__proto__.__proto__ ===null);// true

函数也是同样的道理

function f() {}

console.log(f.__proto__ === Function.prototype); // true
console.log(f.__proto__.__proto__ === Object.prototype); // true

基本数据类型

string、number、boolean

它们不是对象,但是可以访问它们的属性或方法。这是因为在尝试访问时,临时包装器对象将会通过内建的构造器String、Number、Boolean被创建,然后调用它们的方法,调用完成之后,包装器对象将会被销毁。

null和undefined没有对象包装器

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

更改原生原型

尽量不要去修改原生上的方法,只有一种情况允许修改,那就是polyfilling(一个专业术语,表示某个方法在JavaScript规范中已存在,但是特定的JavaScript引擎尚不支持)。

if (!String.prototype.repeat) { // 如果这儿没有这个方法
  // 那就在 prototype 中添加它

  String.prototype.repeat = function(n) {
    // 重复传入的字符串 n 次

    // 实际上,实现代码比这个要复杂一些(完整的方法可以在规范中找到)
    // 但即使是不够完美的 polyfill 也常常被认为是足够好的
    return new Array(n + 1).join(this);
  };
}

alert( "La".repeat(3) ); // LaLaLa

总结:

  • 所有内建的对象都遵循相同的模式(pattern)
    • 方法都存储在 prototype 中(Array.prototype、Object.prototype、Date.prototype 等)
    • 对象本身只存储数据(数组元素、对象属性、日期)
  • 原始数据类型也将方法存储在包装器对象的 prototype 中:Number.prototype、String.prototype 和 Boolean.prototype。只有 undefined 和 null 没有包装器对象。

原型方法,没有__proto__的对象

通过obj.__proto__设置或读区原型被认为已经过时且不推荐使用。

现代方法:

  • Object.getPrototypeOf(obj) --返回对象obj的[[Prototype]]。
  • Object.setPrototypeOf(obj, proto) -- 将对象obj的[[Prototype]]设置为proto。

__proto__不被反对的唯一的用法是在创建新对象时,将其做属性:{__proto__:....}。

还有一种特殊的方法:

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

例如:

let animal = {
  eats:true
}

let rabbit = Object.create(animal);

console.log(rabbit)

console.log(Object.getPrototypeOf(rabbit)===animal);//true

Object.setPrototypeOf(rabbit,{});//将rabbit的原型修改为{}

// 可选参数
let rabbit = Object.create(animal, {
  jumps: {
    value: true
  }
});

Object.create()也可以用来克隆对象。

let clone = Object.create(
  Object.getPrototypeOf(obj),//获取对象的原型属性
  Object.getOwnPropertyDescriptors(obj)//获取对象的所有属性
);

"Very plain" objects

let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // [object Object],并不是 "some value"!

这是因为 __proto__ 是一个保留字,它的值只能是null或者一个对象。其他的值会被忽略。

解决上面的问题有两种方式:

  • 使用Object.create(null),创建一个没有原型的对象。缺点是没有任何内建的对象的方法,例如toString。
  • Map
最后更新:
贡献者: qiuyulc
Prev
对象属性配置
Next
class