JavaScript - 原型、原型链

523 阅读10分钟

重点

五条原型规则

  • 所有的引用类型(数组、对象、函数)都具有对象特性,即可自由扩展属性(除了"null")
  • 所有的引用类型(数组、对象、函数)都有一个__proto_(隐式原型)_属性,属性值是一个普通的对象
  • 所有的函数(只有函数才具有显示原型),都有一个prototype(显示原型)属性,属性值也是一个普通的对象
  • 所有的引用类型(数组、对象、函数), __proto__(隐式原型)属性值 指向它构造函数的prototype(显示原型)属性值
  • 当视图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数prototype)中寻找

普通对象和函数对象

JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。

var o1 = {}; 
var o2 =new Object();
var o3 = new f1();

function f1(){}; 
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object); //function 
console.log(typeof Function); //function  

console.log(typeof f1); //function 
console.log(typeof f2); //function 
console.log(typeof f3); //function   

console.log(typeof o1); //object 
console.log(typeof o2); //object 
console.log(typeof o3); //object

  在上面的例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。

原型模式

理解原型

  无论何时,只要创建一个函数,就会按照特定规则为函数创造一个prototype属性(指向原型对象)。默认情况下,所有的原型对象自动获得一个名为constructor的属性。指回与之相关的构造函数。

原型对象

  所有的原型对象(Person.prototype)自动获得一个名为constructor的属性。指回与之相关的构造函数(Person)。原型对象其实就是普通对象(但 Function.prototype 除外,它是函数对象,但它很特殊,他没有prototype属性)

function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';
Person.prototype.sayName = function() {
  alert(this.name);
}

console.log(Person.prototype); // {name: xxxx,constructor: Person,__proto__: Object}
console.log(Person.prototype.constructor === Person); // true

__proto __  

  JS在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做__proto__(隐式原型) 的内置属性,用于指向创建它的构造函数的原型对象。

function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';
Person.prototype.sayName = function() {
  alert(this.name);
}

// new 操作符会将构造函数自身的属性和方法也赋值给实例对象
let person = new Person();
console.log(person.__proto__ === Person.prototype); // true

构造函数

  在自定义构造函数时,原型对象只会默认获得constructor属性,其他方法都继承自Object。每次使用构造函数创建一个新实例,这个实例内部[[prototype]]指针(即__proto__)会被赋值为构造函数的原型对象(fn.prototype)。实例与构造函数原型之间有直接联系,但实例与构造函数之间没有直接的联系 如图: 原型链

/** 
 * 构造函数可以是函数表达式
 * 也可以是函数声明,因此以下两种形式都可以:
 * function Person() {} 
 * let Person = function() {} 
 */
 function Person() {}
/** 
 * 声明之后,构造函数就有了一个
 * 与之关联的原型对象 普通对象: Person.prototype,
 */
 console.log(typeof Person.prototype); // 'object'
 console.log(Person.prototype); // { constructor: Person, __proto__: Object }
 /** 
 * 如前所述,构造函数有一个 prototype 属性
 * 引用其原型对象,而这个原型对象也有一个
 * constructor 属性,引用这个构造函数
 * 换句话说,两者循环引用:
 */
 console.log(Person.prototype.constructor === Person); // true
 /** 
 * 正常的原型链都会终止于 Object 的原型对象
 * Object 原型的原型是 null 
 */
 console.log(Person.prototype.__proto__ ===   Object.prototype); // true
 console.log(Person.prototype.__proto__.constructor === Object); // true
 console.log(Person.prototype.__proto__.__proto__ === null); // true

 /** 创建新实例 */
 let person = new Person();
 let person1 = new Person();
 let person2 = new Person();
 /** 
 * 构造函数、原型对象和实例
 * 是 3 个完全不同的对象:
 */
 console.log(person !== Person); // true
 console.log(person !== Person.prototype); // true
 console.log(Person.prototype !== Person); // true
 /** 
 * 实例通过__proto__链接到原型对象,
 * 它实际上指向隐藏特性[[Prototype]] 
 * 
 * 构造函数通过 prototype 属性链接到原型对象
 * 
 * 实例与构造函数没有直接联系,与原型对象有直接联系
 */
 console.log(person1.__proto__ === Person.prototype); // true 
 console.log(person1.__proto__.constructor === Person); // true

 /** 
 * 同一个构造函数创建的两个实例
 * 共享同一个原型对象:
 */ 
console.log(person1.__proto__ === person2.__proto__); // true

/** 
 * instanceof 检查实例的原型链中
* 是否包含指定构造函数的原型:
 */ 
console.log(person1 instanceof Person); // true 
console.log(person1 instanceof Object); // true 
console.log(Person.prototype instanceof Object); // true

原型对象之间的关系

原型层级

  在实例中访问某个属性,若该实例自身没有该属性,则该实例会将请求委托给构造函数的原型对象,一级一级进行查到,知道找到或者找不到。实例重写原型上的属性或方法,会遮蔽原型对象的同名属性或方法。

function Person() {} 
Person.prototype.name = "Nicholas";
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function() { 
 console.log(this.name); 
}; 
let person1 = new Person(); 
let person2 = new Person(); 
console.log(person1.hasOwnProperty("name")); // false 

person1.name = "Greg"; 
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true 

console.log(person2.name); // "Nicholas",来自原型
console.log(person2.hasOwnProperty("name")); // false 

delete person1.name; 
console.log(person1.name); // "Nicholas",来自原型
console.log(person1.hasOwnProperty("name")); // false

  通过调用 hasOwnProperty()能够清楚地看到访问的是实例属性还是原型属性。 调用 person1.hasOwnProperty("name")只在重写 person1 上 name 属性的情况下才返回 true,表明此时 name 是一个实例属性,不是原型属性。

其他原型语法

  前面所有的例子中,每次定义一个属性或方法都会把Person.prototype重写一次。为了减少代码冗余,也为了从视觉上更好地封装原型功能,可以直接通过一个包含所有属性和方法的对象字面量来重写原型。

function Person() {} 
Person.prototype = {
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
   console.log(this.name); 
 } 
};
console.log(Person.prototype.constrcutor); // Object
let person = new Person();
console.log(person.constructor == Person); // false
console.log(person.constructor == Object); // true

  这种写法存在一个问题:重写了Person.prototype之后,原型对象只会默认获得constructor属性就不再指向Person而是Object',原型对象自由属性不再包含constructor。字面量方式需要显示指定constructor属性的值   修复constructor指向

// 方法一:存在问题: 自动生成的constructor属性[[Enumerable]]特性为false,但是字面方式恢复的constructor属性的[[Enumerable]]特性为true
function Person() { 
} 
Person.prototype = { 
 constructor: Person, 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 	console.log(this.name); 
 } 
};
// 方法二
function Person() {} 
Person.prototype = { 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 console.log(this.name); 
 } 
}; 
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", { 
 enumerable: false, 
 value: Person 
});

原型的动态性

其原因可以归结为实例与原型之间的松散连接关系,因为实例与原型之间的连接只不过是一个指针,而非一个副本。 请记住:实例中的指针仅指向原型,而不指向构造函数。   调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。

function Parent(){} 
// friend的__proto__仍旧为默认的prototype的指针, 指向原来的堆内存
var friend = new Parent(); 
// 重新原型相当于在堆内存中新建一个对象,将Parent的prototype指针修改为新对象堆内存
Parent.prototype = { 
    name : "Nicholas", 
    age : 29, 
    job: "Software Engineer", 
    sayName : function () { 
        console.log('Prototype Function');
    } 
};
console.log(friend);
console.log(Parent);
console.log(friend.name); // undefiend 

原型动态性

原型(原型链)的问题

  原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性。原型对象上的所有属性和方法在实例之间是共享的,原型对象上的引用值被一个实例修改了,也会体现在其他实例上

function Person() {} 
Person.prototype = { 
 constructor: Person, 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 friends: ["Shelby", "Court"],
 sayName() { 
 	console.log(this.name); 
 } 
}; 
let person1 = new Person(); 
let person2 = new Person(); 
person1.friends.push("Van"); 
console.log(person1.friends); // "Shelby,Court,Van" 
console.log(person2.friends); // "Shelby,Court,Van" 
console.log(person1.friends === person2.friends); // true

原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。原型链的基本构想:原型A对象本身有一个内部指针__proto__指向另一个原型对象B,相应地原型B也有一个指针__proto__指向另一个原型对象C。这样就在实例和原型之间构造了一条原型链。 默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的 原型链

原型相关方法

Object.create(proto,[propertiesObject])

  • proto: 新创建对象的原型对象
  • propertiesObject:可选, 需要传入一个对象,要定义其可枚举属性或修改的属性描述符的对象。对象中存在的属性描述符主要有两种:数据描述符访问器描述符, 即新对象的属性
  • 返回值: 一个新对象,带着指定的原型对象和属性。   Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。返回一个新对象,带着指定的原型对象和属性。
function Animal() {
  this.type = 'animal';
}
Animal.prototype.makeSound = function () {
  console.log('makeSound');
}

function Dog() {
  this.name = 'dog';
}
// 子类继承父类 - 相当于将Dog的原型对象重写,会导致Constructor也指向了Animal
Dog.prototype = Object.create(Animal.prototype);
console.log(Dog.prototype.constructor); // f Animal {}
// 修复constructor指向
Dog.prototype.constructor = Dog;
let dog = new Dog();
console.log('dog', dog);

// propertiesList
let o = {};
o = Object.create(Animal.prototype, {
  foo: {
    writable: true,
    configurable: true,
    value: 'fooo'
  }
});
console.log('o', o); // { foo: 'fooo', __proto__: { makeSound: f(), constructor: f Animal() } }

Object.getPrototypeOf(object)

  • 要返回其原型的对象
  • 返回值:给定对象的原型。如果没有继承属性,则返回null   Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)
var proto = {};
let result1 = Object.getPrototypeOf(proto);
console.log('result1', result1); // 表示Object.prototype原型对象
console.log('object', Object.prototype);
function Animal() {
  this.type = 'animal';
}
Animal.prototype.makeSound = function () {
  console.log('makeSound');
}
let result = Object.getPrototypeOf(Animal);
console.log('result', result); // 指向Function.prototype
console.log(Animal.__proto__.constructor); // f Function (){}

Object.setPrototypeOf(obj, prototype)

  • obj: 要设置其原型的对象
  • prototype: 该对象的新原型(一个对象 或 null).

由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.proto = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象   不推荐使用,不进行测试

Object.getOwnPropertyNames(obj)

  • obj: 一个对象,其自身的可枚举和不可枚举属性的名称被返回。
  • 返回值:在给定对象上找到的自身属性对应的字符串数组。   Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组
let obj = {
  name: '焦糖瓜子',
  age: 18,
  sex: 'female'
};
Object.defineProperty(obj, 'job', {
  value: '前端搬砖工'
});
console.log('getOwnPropertyNames', Object.getOwnPropertyNames(obj)); // ["name", "age", "sex", "job"]

Object.prototype.hasOwnProperty(prop)

  • prop: 要检测的属性的 String 字符串形式表示的名称,或者 Symbol。   hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。原型对象上的属性和方法可直接使用 对象本身去获取和执行in关键字可遍历在原型链上的属性
function Animal() {
  this.type = 'animal';
}
Animal.prototype.makeSound = function () {
  console.log('makeSound');
}
const dog = new Animal();
dog.name = '小狗狗';
console.log('dog', dog);
console.log('hasOwnProperty', dog.hasOwnProperty('makeSound')); // false
console.log('hasOwnProperty', 'makeSound' in dog); // true
console.log('hasOwnProperty', dog.hasOwnProperty('name')); // true

Object.prototype.isPrototypeOf(object)

  • 语法:prototypeObj.isPrototypeOf(object)
  • 返回值:Boolean,表示调用对象是否在另一个对象的原型链上。   isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上

isPrototypeOf() 与 instanceof 运算符不同。在表达式 "object instanceof AFunction"中,object 的原型链是针对AFunction.prototype 进行检查的,而不是针对 AFunction 本身

function  Animal () {
  this.type = 'animal';
}

const dog = new Animal();
console.log('isPropertyOf', Animal.prototype.isPrototypeOf(dog)); // true
console.log('isPropertyOf', Function.prototype.isPrototypeOf(dog)); // false
console.log('isPropertyOf', Object.prototype.isPrototypeOf(dog)); // true
console.log('isPropertyOf', Function.prototype.isPrototypeOf(Animal)); // true
console.log('isPropertyOf', Object.prototype.isPrototypeOf(Function)); // true

console.log('instanceof', dog instanceof Animal); // true
console.log('instanceof', dog instanceof Object); // true

instanceof

f instanceof Foo的判断逻辑:
1、f 的__ptoto__一层一层往上,能否对应到Foo.prototype
2、再试着判断f instanceof Object   思维导图