乐趣区

js回顾:原型链

原型链
原型链实际上是 JavaScript 中的实现继承的机制,在搞懂原型链之前首先要搞懂几个概念:this,普通对象和函数对象,构造函数,new
this
this 对于很多人来说是混杂不清的概念,但是想要弄清楚原型链我们必须了解什么是 this
首先,this 只能在存在与函数中
其次,this 其实是当前函数所在上下文环境,再简单一点也可以理解为 this 返回一个当前函数所在的对象,也就是说想要知道 this 是什么我们只需要关注是谁调用了 this 所在的函数就可以了
如下边的代码 zhao.sayName()是 zhao 调用的 sayName 函数所以 sayName 中的 this 自然指的就是 zhao 这个对象,而下方 var liName = zhao.sayName 语句是将 sayName 这个函数赋值给 liName,调用 liName 就相当于在最顶层也就是 window 下直接调用 sayName,this 的指向自然就是 window 这个最顶层对象
var name = “Li”
var zhao = {
name: “Zhao”,
sayName: function () {
console.log(this.name);
}
}
zhao.sayName() // Zhao
var liName = zhao.sayName;
liName() // Li
普通对象与函数对象
JavaScript 中一切都可以看作对象,但是实际上对象也是有区别的,对象分为普通对象和函数对象
// 普通对象
var o1 = {}
var o2 = new Object()
var o3 = new f1()
// 函数对象
function f1(){}
var f2 = function(){}
var f3 = new Function()

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

console.log(typeof o1); //object
console.log(o1.prototype); //undefined
console.log(typeof o2); //object
console.log(o2.prototype); //undefined
console.log(typeof o3); //object
console.log(o3.prototype); //undefined
凡是通过 function 构建的对象都是函数对象, 并且只有函数对象才有 prototype 属性,普通对象没有
prototype 原型
prototype 又是什么呢?
当我们创建函数的时候,编译器会自动为该函数创建一个 prototype 属性,这和属性指向一个包含 constructor 属性的对象,而这个属性又默认指回原函数,读起来有点绕对吧,大概是这样的
function Person() {
// prototype = {
// constructor: Person,
// }
}
每个函数对象都有一个 prototype(原型)属性,在我看来 prototype 属性的意义:

创建对象的模板
公开的共享空间

这两点等学习了下边 new 命令你就会明白了
constructor 构造函数
函数对象的一种用法就是构造函数,通过构造函数可以构建一个函数对象的实例(普通对象)
function Person(name, age){
this.name = name;
this.age = age;
this.sayHello = function(){
console.log(`Hello! my name is ${this.name}`);
};
}

var person1 = new Person(“kidder”, 28);
person1.sayHello(); // Hello! my name is kidder
console.log(person1.constructor); //[Function:Person]
按照惯例,构造函数的命名以大写字母开头,非构造函数以小写字母开头, 通过构造函数构造的普通对象都会有一个 constructor(构造函数)属性,该属性指向构造该对象的构造函数
new 命令
new 命令的工作机制

创建一个空对象作为要返回对象的实例
将这个空对象的原型 (__ proto __) 指向构造函数的 prototype 属性
将这个空对象赋值给构造函数内部的 this
执行构造函数内部的代码

原型链
下面我们来看看构造函数构建一个普通对象的时候发生了什么
var Person = function (name) {
this.name = name;
this.age = 18;
};
Person.prototype.sayHello = function(){
console.log(`Hello! my name is ${this.name}`);
};
var li = new Person(“Li”);
console.log(li.name); // Li
console.log(li.age); // 18
li.sayHello(); // Hello! my name is Li
创建一个空对象作为要返回对象的实例
{}
将这个空对象的原型 (__ proto __) 指向构造函数的 prototype 属性
{
__proto__:Person.prototype;
}
将这个空对象赋值给构造函数内部的 this
this = {
__proto__:Person.prototype;
}

执行构造函数内部的代码
this = {
__proto__:Person.prototype;
name: “Li”;
age: 18;
}

所以 li 这个对象中只有 name 和 age 两个属性,为什么 li.sayHello()会输出 Hello! my name is Li 呢?
这就是原型链,当给定的属性在当前对象中找不到的情况下,会沿着__proto__这个属性一直向对象的上游去寻找,直到__proto__这个属性指向 null 为止,如果找到指定属性,查找就会被截断,停止

上面这张图是整个 JavaScript 的原型链体系,为了让这张图更直观所以我将构造函数的 prototype 属性单独提了出来,恩,其实画在构造函数内部也可,但同时因为对象是引用类型,所以这样画也没毛病吧

proto 和 prototype
这两个属性经常会被我们混淆,那么我们回过头再来总结一下
prototype:只有函数对象才具有的属性,它用来存放的是构造函数希望构建的实例具有的共享的属性和方法,主要用于构造函数的实例化
proto : 所有对象都具有的属性,它指向的是当前对象在原型链上的上级对象,主要作用是让编译器在由__proto__这个属性构成的原型链上查找特定的属性和方法
补充
prototype 的共享属性
var Person = function (name) {
this.name = name;
this.age = 18;
};
Person.prototype.sayHello = function(){
console.log(`Hello! my name is ${this.name}`);
};
var li = new Person(“Li”);

var Person1 = function () {
};
Person.prototype.name = “Li”
Person.prototype.age = 18
Person.prototype.sayHello = function(){
console.log(`Hello! my name is ${this.name}`);
};

var Li = new Person1();

关于 Person 和 Person1 两种构造函数的写法有什么不同呢?

一般来说写在 prototype 原型对象中的属性和方法都是公用的,也就是说写在构造函数中的属性在构建普通对象的时候,都会在新对象中重新定义,也就是从内存的角度来说又会多占用一些内存空间,所以我们将构造函数的所有属性和方法都写在 prototype 原型中不好吗?
但是原型函数也是有缺点的:
不够灵活
var Person = function () {
};
Person.prototype.name = “Li”
Person.prototype.age = 18
Person.prototype.sayHello = function(){
console.log(`Hello! my name is ${this.name}`);
};

var li = new Person();
var zhao = new Person();
这种方式构造的所有对象都是一个模板,虽然我们也可以在当前对象下进行修改,但这样一点也不优雅,不规整,而且从某种意义上来说也是对内存的浪费
对于引用类型的修改会被全部共享
var Person = function () {
};
Person.prototype.name = “Li”
Person.prototype.age = 18
Person.prototype.friends = [“ZhangSan”, “LiSi”]
Person.prototype.sayHello = function(){
console.log(`Hello! my name is ${this.name}`);
};

var li = new Person();
var zhao = new Person();
li.friends.push(“WangWu”);
console.log(zhao.friends); // [‘ZhangSan’, ‘LiSi’, ‘WangWu’]
在 JavaScript 中,基本类型的修改可以明确的通过创建或修改在当前对象下的属性对原型链进行截断,但是像数组,对象这种引用类型的值虽然也可以通过在当前对象中创建该属性来对原型链进行截断,但是一不注意就可能会出现上面这种情况直接对原型进行了修改
构造函数与原型相结合
所以,用构造函数来定义实例属性,用原型定义方法和共享的属性,这样写就比较优雅了
function Person(name, age){
this.name = name;
this.age = age;
this.friends = [“ZhangSan”, “LiSi”];
}
Person.prototype.sayHello = function(){
console.log(`Hello! my name is ${this.name},${this.age}岁了 `);
};
var li = new Person(“li”, 18);
var zhao = new Person(“zhao”, 16);
li.sayHello();
// Hello! my name is li, 18 岁了
zhao.sayHello();
// Hello! my name is zhao,16 岁了
li.friends.push(“WangWu”);
console.log(zhao.friends);
// [‘ZhangSan’, ‘LiSi’]
创建对象的几种方式
构造函数方式法一用构造函数构造一个新对象

var A = function () {};
var a = new A();
console.log(a.constructor); // [Function:A]
console.log(a.__proto__ === A.prototype); //true

字面量方式法二的本质来说和法一是一样的, 就是隐式调用原生构造函数 Object 来构造新对象

var a = {};
// var a = new Object();
console.log(a.constructor); // [Function:Object]
console.log(a.__proto__ === Object.prototype); //true

create 方式法三 Object.create 是以一个普通对象为模板创建一个新对象

var a1 = {a:1}
var a2 = Object.create(a1);
console.log(a2.constructor); // [Function:Object]
console.log(a2.__proto__ === a1);// true
console.log(a2.__proto__ === a1.prototype); //false

所以除了 Object.create 创建对象的方式,可以说:__ proto __ === constructor.prototype;
constructor
前面我们说道 prototype 的时候进行原型属性的赋值的时候,采用的是逐项赋值,那么当我直接将对象赋值给 prototype 属性的时候会发生什么呢?
function Person() {}
Person.prototype = {
name : “Li”,
age : 18,
sayHello : function () {
console.log(`Hello! my name is ${this.name},${this.age}岁了 `);
}
};
var li = new Person();
console.log(li instanceof Object); // true
console.log(li instanceof Person); // true
console.log(li.constructor === Person); // false
console.log(li.constructor === Object); // true
console.log(Person.prototype.constructor); // Object
这时候我们就发现我们构建的 li 对象的 constructor 不再指向它的构造函数 Person,而是指向了 Object, 并且 Person 原型 Person.prototype 的 constructor 指向也指向了 Object,这是什么原因呢?
其实,根源出现在 Person.prototype 上,上边我们提到过,其实我们在写构造函数的时候实际上是这样的
function Person() {
// prototype = {
// constructor : Person
// }
}
当我们构建 Person 构造函数的时候,编译器会自动生成一个带有指向 Person 的 constructor 属性的对象,并把这个对象赋值给 Person.prototype,我们又知道 js 中对象是引用类型,当我们使用 Person.prototype.name=… 的时候实际上是对这个对象的修改,而使用 Person.prototype={…}实际上是将这个属性原本的指针指向了另一个新创建的对象而不是原来编译器自动创建的那个:

而 li 的 constructor 属性自然是继承自 Person.prototype, 所以 constructor 自然也就跟着改变了,如果在编程的过程中 constructor 这个属性很重要的话可以通过下面的方式
function Person() {}
Person.prototype = {
constructor:Person
name : “Li”,
age : 18,
sayHello : function () {
console.log(`Hello! my name is ${this.name},${this.age}岁了 `);
}
};

var li = new Person();
console.log(li instanceof Object); // true
console.log(li instanceof Person); // true
console.log(li.constructor === Person); // true
console.log(li.constructor === Object); // false
console.log(Person.prototype.constructor); // Person
结语:
参考:《JavaScript 高级程序设计》
这是我对 JS 原型链部分的总结与思考,也是我写的第一篇这么正式的技术文档,如有纰漏之处,欢迎大家批评指正

退出移动版