原型和原型链的深入浅出

163 阅读11分钟

原型和原型链

参考网站

一、万物皆对象?

  • 什么是对象?

对象是 javascript 基本数据类型。对象是一种复合值: 它将很多值(原始值或者其它对象)聚合在一起,可通过名字访问这些值。

JavaScript 中,对象是拥有属性和方法的数据

JavaScript 对象是动态的可以新增属性和删除属性

1.1 数据类型

基本类型(原始类型):Undefined,Null,Boolean,Number,String

引用类型(对象类型):Object,Array,Date,RegExp,Function,特殊的基本包装类型(String、Number、Boolean)以及单体内置对象(Global、Math)

1.2 基本类型与引用类型区别

  1. 基本类型值不可变,引用类型值可变

// example1.js

var str = 'You can you up'
str.toLowwer()
console.log(str)


// example2.js
var str = 'You can you up'
str.name = 'LW'
str.toString = function(){}

console.log(str.name)
console.log(str.toString)

// example3.js
var person = {};
person.name = 'jeff';
person.age = 18;
person.sayName = function(){console.log(person.name);}
person.sayName();

delete person.name;
person.sayName();
  1. 基本类型的比较是值的比较, 引用类型的比较是引用的比较

var a = 1;
var b = true;
console.log(a == b)

var a = 'jozo';
var b = 'jozo';
console.log(a === b)

# example4.js
var person1 = {};
var person2 = {};
console.log(person1 == person2)
  1. 复制变量时

原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的 value 而已。

引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量, 也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。


# example5.js

var a = 1
var b = a
b=2
console.log(a)

var obj1 = {name:'jeff',age:18}
var obj2= obj1
obj2.age=20
console.log(`obj1=${obj1}`)
  1. 参数传递的不同

原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。

引用值:对象变量它里面的值是这个对象在堆内存中的内存地址


# example6.js
var a=1
var b = {name:'jeff'}

function set(obj){
    obj.age = 20
}
set(a)
set(b)
console.log(a)
console.log(b)

1.3 原始值是对象吗?


var a= `
  产品:'这个需求什么时候做完?'
  程序猿:'下班前做完'
  第二天...
  产品:'你昨天说下班需求做完,到现在你也没做完啊!',
  程序猿:'我还没下班了。'
`
console.log(a.length) // 89

在这里 a 只是一个字符串,不应该存在属性和方法,但事实上他有自己的属性和方法,为什么?

1.4 包装对象

其实在上面的例子中在读取字符串的时候会创建一个对象,但是这个对象只是临时的,所以我们称它为临时对象,学术名字叫包装对象。说它临时,是因为我们在读取它的属性的时候,js 会把这个 string 字符串通过 new String()方式创建一个字符串对象,有了对象自然就有了属性,但是这个对象只是临时的,一旦引用结束,这个对象就被销毁了。



var str = "You can you up"
console.log(str.length)

// 类似于下面的代码
console.log(new String(str).length)

同理数字、布尔值在读取属性的时候也可以通过自己的构造函数来创建自己的一个临时对象

思考



typeof null

typeof undefined

1.5 1 总结

JavaScript 除 undefined 外 几乎所有事物都是对象或看成对象(number,string,boolean)

二、构造函数

Javascript 中的函数即可以当普通函数调用,也可以是构造函数,普通函数通过 new 调用时, 它就是一个构造函数,即构造函数就是一个普通函数

2.1 构造函数与普通函数的区别

  1. 在命名规则上,构造函数一般是首字母大写,普通函数一般是驼峰命名法;

  2. 调用时,普通函数是直接调用,构造函数通过 new 关键字来生成新的实例(对象)(函数对象);

  3. 构造函数使用 this 定义成员变量和成员方法,普通函数中不使用 this 关键字定义成员变量和方法。

  4. 返回值,普通函数可以有 return, 构造函数一般没有返回值;

  5. 构造函数没返回值时,返回实例对象,有返回值时,校验返回值类型,当为非引用类型时,返回的还是实例对象,当为引用类型时,返回该引用类型


// example7.js

function Person(name, age) {
  this.name = name
  this.age = age
  this.sayName = function(){
    console.log(this.name,`..`)
  }
}

let person1 = new Person('lw', 30)
person1.sayName()


// example8.js

function Person(name, age) {
  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }
  return 'hello'
  return {
    a: '1',
    b: '2'
  }
}

let person1 = new Person('lw', 30)
console.log(person1)

2.2 new 关键字

new 执行过程

  let person = new Person('lw',30)

  //伪代码
  var obj = {}
  obj.__proto__ = Person.prototype
  Person.call(obj)
  return obj
  1. 创建空对象
  2. 将空对象的 __proto__ 指向构造函数的 prototype 对象(在此建立  原型链 obj->Person.prototype->Object.prototype->null)
  3. 改变作用域,即将 Person 函数 this 指向新对象 obj,使 obj 继承 Person 的属性和方法,并调用 Person 函数(obj.Person('lw',30))
  4. 判断返回值,返回新对象

2.3 内置构造函数(构造器)

Javascript 内置构造函数有很多,主要是基本类型、引用类型(复合类型)

Object


var obj = new Object()

Function


var sum = Function("a","b","console.log(a+b)")
sum(1,2)

Array


var arr = new Array()

RegExp


var reg = new RegExp("\\d","g")


Number、 String、 Boolen


var num = new Number(20)
var str = new String("test")
var bool = new Boolean(false)

2.4 普通对象与函数对象

凡是通过 new Function()创建的对象都是函数对象,其它的都是普通对象。


// example9.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);
console.log(typeof Function);

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

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

上例中,f1,f2,f3 都是函数对象,o1,o2,03 都是普通对象, Object,Function 也是函数对象.

函数对象可以创建普通对象,普通对象无法创建函数对象。


function foo(){}
typeof foo

var f1 = new foo()
typeof f1

三、原型和原型链

3.1 instanceof

instanceof 运算符用来判断一个构造函数的 prototype 属性所指向的对象是否存在另外一个要检测对象的原型链上


  var obj = {}
  obj instanceof Object // 检测Object.prototype是否存在于参数obj的原型链上。


// example10.js

Function instanceof Object
Function instanceof Function

Object instanceof Function
Object instanceof Object

Function.prototype
Object.prototype

Object 继承自己, Function 继承自己,Object 和 Function 互相继承对方。


// example11.js

function Person() {}

var person1 = new Person()

console.log(person1 instanceof Person)
console.log(person1 instanceof Object)

3.2 constructor 属性


console.log(person1.constructor == Person)

每个实例对象都有 constructor 属性,且 constructor 属性指向构造函数本身。

3.3 原型对象(显示原型)

3.3.1 什么是原型对象?

在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个 prototype 属性,这个属性是一个指针,他指向一个对象,这个对象就是原形对象

每个函数对象都有 prototype 属性

原型对象,就是一个普通对象。原型对象就是 Person.prototype。


function Person() {
  this.name = 'lw'
  this.age = 30
  this.sayName = function () {
    console.log(this.name)
  }
}

// 改写
function Person(){

}

Person.prototype.name = 'lw'
Person.prototype.age = 30
Person.prototype.sayName = function(){
  console.log(this.name)
}

// 再次改写
Person.prototype = {
  name: 'lw',
  age: 30,
  sayName: function () {
    console.log(this.name)
  }
}

在  默认情况下,所有的原型对象都会自动获得一个 constructorn 属性,这个属性是一个指针,指向 prototype 所在的构造函数


Person.prototype.constructor == Person

前面提到过


person1.constructor =Person

person1 有 constructor 属性,是因为 person1 是 Person 的实例。

那 Person.prototype 为什么有 constructor 属性?同理, Person.prototype (你把它想象成 A) 也是 Person 的实例。 也就是在 Person 创建的时候,创建了一个它的实例对象并赋值给它的 prototype,基本过程如下:


var A = new Person();
Person.prototype = A;

原型对象(Person.prototype)是构造函数(Person)的一个实例

但 Function.prototype 除外,它是函数对象,但它很特殊,他没有 prototype 属性(前面说道函数对象都有 prototype 属性)

function Person(){};
 console.log(Person.prototype)
 console.log(typeof Person.prototype)
 console.log(typeof Function.prototype)
 console.log(typeof Object.prototype)
 console.log(typeof Function.prototype.prototype)

Function.prototype 为什么是函数对象呢?


var A = new Function ();
Function.prototype = A;

上文提到凡是通过 new Function( ) 产生的对象都是函数对象。因为 A 是函数对象,所以 Function.prototype 是函数对象。

3.3.2 原型对象是用来做什么的呢?

主要用于继承

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。


// example12.js

function Person() {}
Person.prototype.name = 'lw';
var person = new Person();
person.name = 'zk';
console.log(person.name)
delete person.name;
console.log(person.name)

console.log(person.toString())
console.log(Object.prototype)

可以让所有的实例对象共享它所包含的属性和方法,减少内存占用


// example13.js

function A() {
  this.name = 'lw'
  this.sayName = function () {
    return this.name
  }
}

function B() {
  this.name = 'zk'
}

B.prototype.sayName = function () {
  return this.name
}

var a1 = new A()
var a2 = new A()
var b2 = new B()
var b2 = new B()

console.log(a1)
console.log(a2)
console.log(b1)
console.log(b2)

我们通过使用构造函数 A 创建了两个对象,分别是 a1 , a2 ;通过构造函数 B 创建了两个对象 b1 , b2 ;我们可以发现 b1 , b2 这两个对象的那个 sayHello 方法都是指向了它们的构造函数的 prototype 属性的 sayHello 方法.而 a1 , a2 都是在自己内部定义了这个方法.

定义在构造函数内部的方法,会在它的每一个实例上都克隆这个方法;定义在构造函数的 prototype 属性上的方法会让它的所有示例都共享这个方法,但是不会在每个实例的内部重新定义这个方法 .如果我们的应用需要创建很多新的对象,并且这些对象还有许多的方法,为了节省内存,我们建议把这些方法都定义在构造函数的 prototype 属性上 当然,在某些情况下,我们需要将某些方法定义在构造函数中,这种情况一般是因为我们需要访问构造函数内部的私有变量

总结

每个构造函数都有一个原型对象,原型对象都包含一个指针,指向构造函数,而实例对象都包含一个指向原型对象的内部指针。


function Person(){}

Person.prototype.toString = function (){}

var person1 = new Person()

Person.prototype.constructor = Person

person1.constructor = Person

3.4 __proto__(隐式原型)

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

对象 person1 有一个 __proto__属性,创建它的构造函数是 Person,构造函数的原型对象是 Person.prototype ,所以: person1.__proto__ == Person.prototype

3.5 函数对象

所有函数对象的 __proto__ 都指向 Function.prototype,它是一个空函数


// example14.js

function Person() {}
console.log(Person.__proto__ === Function.prototype) //true

console.log(Number.__proto__ === Function.prototype) // true

console.log(String.__proto__ === Function.prototype) // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype // true
Object.constructor == Function // true

Function.__proto__ === Function.prototype // true
Function.constructor == Function //true


所有的构造器都来自于 Function.prototype,甚至包括根构造器 Object 及 Function 自身。所有构造器都继承了·Function.prototype·的属性及方法。如 length、call、apply、bind


Function.prototype.__proto__ === Object.prototype

所有的构造器也都是一个普通 JS 对象,可以给构造器添加/删除属性等。同时它也继承了 Object.prototype 上的所有方法:toString、valueOf、hasOwnProperty 等


Object.prototype.__proto__ === null

3.6 继承

当我们创建一个函数时:


var Person = new Object()
console.log(Person.prototype)

Person 是 Object 的实例,所以 Person 继承了 Object 的原型对象 Object.prototype 上所有的方法。

当我们定义一个数组时:


var arr = new Array()
console.log(Arr.prototype)

继承了 Array 的原型对象 Array.prototype 上所有的方法.

此处输出是空数组,可以使用 Object.getOwnPropertyNames 获取所有(包括不可枚举的属性)的属性名不包括 prototy 中的属性,返回一个数组:

var arrayAllKeys = Array.prototype; // [] 空数组
// 只得到 arrayAllKeys 这个对象里所有的属性名(不会去找 arrayAllKeys.prototype 中的属性)
console.log(Object.getOwnPropertyNames(arrayAllKeys));
/* 输出:
["length", "constructor", "toString", "toLocaleString", "join", "pop", "push",
"concat", "reverse", "shift", "unshift", "slice", "splice", "sort", "filter", "forEach",
"some", "every", "map", "indexOf", "lastIndexOf", "reduce", "reduceRight",
"entries", "keys", "copyWithin", "find", "findIndex", "fill"]
*/

细心的你肯定发现了 Object.getOwnPropertyNames(arrayAllKeys) 输出的数组里并没有 constructor/hasOwnPrototype 等对象的方法(你肯定没发现)。 但是随便定义的数组也能用这些方法


var num = [1];
console.log(num.hasOwnPrototype())

因为 Array.prototype 虽然没这些方法,但是它有原型对象(__proto__)


Array.prototype.__proto__ == Object.prototype

所以 Array.prototype 继承了对象的所有方法,当你用 num.hasOwnPrototype()时,JS 会先查一下它的构造函数 (Array) 的原型对象 Array.prototype 有没有有 hasOwnPrototype()方法,没查到的话继续查一下 Array.prototype 的原型对象 Array.prototype.__proto__有没有这个方法

3.7 原型链


function Person(){}
var person1 = new Person();
console.log(person1.**proto** === Person.prototype); // true
console.log(Person.prototype.**proto** === Object.prototype) //true
console.log(Object.prototype.**proto**) //null

Person.**proto** == Function.prototype; //true
console.log(Function.prototype)// function(){} (空函数)

var num = new Array()
console.log(num.**proto** == Array.prototype) // true
console.log( Array.prototype.**proto** == Object.prototype) // true
console.log(Array.prototype) // [](空数组)
console.log(Object.prototype.**proto**) //null

console.log(Array.**proto** == Function.prototype)// true

3.7 总结

原型和原型链是 js 实现继承的一种模式

原型链的形成是靠__proto__,而非 prototype