JavaScript中的每个对象都有一个特殊的内置属性
[[prototype]]
,它实际上是一个对象,指向的是另外一个对象
[[prototype]]
的作用
get
的对象操作[[prototype]]
指向的对象上,有没有这个属性let obj = {
a: 100,
b: 200,
};
//__proto__是浏览器自动加的
console.log(obj.__proto__);
//这是标准获取方法
console.log(Object.getPrototypeOf(obj));
所有的函数都有一个prototype属性 注意不是
__proto__
function Foo(){
}
/*
通过new方法创建的对象,内部会帮我们做以下事情
1.首先在函数内部创建一个空对象
2.将对象的__proto__(隐式原型)指向函数的prototype(显式原型的作用)
3.将空对象赋值给this
4.执行函数体中的代码
5.将这个对象默认返回
*/
let newFoo = new Foo()
//通过步骤2我们可以得知,newFoo对象的隐式原型就是Foo函数的显式原型
newFoo.__proto__ === Foo.prototype
//无论通过Foo函数创建多少对象,这些对象的隐式原型都是相等的,均指向Foo函数的显式原型
通过以上代码,将函数的显式原型赋值给对象的隐式原型就是 显式原型的作用;
那么知道了它的作用,接下来可以看一个小案例,加深以下体会(将 方法放在原型上)
首先我们知道,我们可以通过构造函数来创建一个又一个的对象,这是为了将多个对象的共同的内容抽象到一起
//创建一个学生构造函数
function Student(stuName,age){
this.stuName = stuName;
this.age = age;
//每个学生都有这个方法
this.study = function () {
console.log(this.stuName + "正在学习");
};
}
//这样我们就可以创建多个对象
let stu1 = new Student("zhangsan",18)
let stu2 = new Student("zhangsan2",18)
let stu3 = new Student("zhangsan3",18)
let stu4 = new Student("zhangsan4",18)
但是新的问题随之而来,每创建一个对象,都会生成一个study的方法,当生成足够多对象的时候,会占用内存
那么通过 显式原型和隐式原型的关系我们可以知道,将共有的方法提取到 显式原型上即可
function Student(stuName, age) {
this.stuName = stuName;
this.age = age;
}
Student.prototype.study = function () {
//这里的this指向,在前面的文章解释过,通过隐式绑定,指向的就是调用方法的对象
console.log(this.stuName + "正在学习");
};
let stu1 = new Student("zhangsan", 20);
let stu2 = new Student("lisi", 18);
console.log(stu1.stuName, stu1.age);
console.log(stu2.stuName, stu2.age);
stu1.study()
stu2.study()
通过 将方法提取到构造函数的显式原型上,就可以解决以上的问题,以下是模拟的内存图
通过内存模拟图我们可以看出
因此当多个对象拥有共同的值时,我们可以放到构造函数的显式原型中
显式原型中有一个属性construtor
我们打印显式原型,会发现有一个construtor的属性,同时打印这个属性发现是函数本身
function Foo(){
}
console.log(Foo.prototype)//construtor
console.log(Foo.prototype.construtor)//Foo
试着画出以下代码的内存图
function Student(stuName, age) {
this.stuName = stuName;
this.age = age;
}
Student.prototype.study = function () {
console.log(this.stuName);
};
let stu1 = new Student("zhangsan", 20);
let stu2 = new Student("lisi", 18);
stu1.address = "河北";
stu1.num = "18213";
Student.prototype.classRoom = "ruanjian";
stu1.classRoom = "jisuanji";
console.log(stu2.classRoom);
stu1.study();
当我们要再函数显式原型上 添加大量的属性以及方法的时候,可以考虑重写显式原型
function Student(stuName, age) {
this.stuName = stuName;
this.age = age;
}
Student.prototype.study = function () {
console.log(this.stuName);
};
Student.prototype.num1 = 123;
Student.prototype.num2 = 456;
//可以这样重写显式原型对象
Student.prototype = {
study:function () {
console.log(this.stuName);
},
num1:123,
num2:456
}
但是重写完只会,我们会发现 重写的显式原型对象,缺失了constructor属性
Student.prototype = {
study:function () {
console.log(this.stuName);
},
num1:123,
num2:456,
//可以直接这样写上该属性
constructor:Student
}
//但是原本的constructor属性,是不可枚举的,且数据属性描述符需要特殊设置
Object.defineProperty(Student.prototype,constructor,{
value:Student
//设置其他的数据属性描述符即可
})
面向对象的三大特性:封装、继承、多态
默认形式的原型链
//当我们创建一个对象的时候
//这种创建方式相当于:let obj = new Object()
//通过上面的学习,我们可以知道obj.__proto__ === Object.prototy
//而Object.prototype作为对象也有自己的隐式原型,它的隐式原型指向null
let obj = {
name:"zhangcheng"
}
因此通过以上理论,我们可以对原型对象进行改造
//当我们在obj上面查找message的时候,自身没有,就会顺着去查找它的原型,一层一层的往上找
let obj = {
name: "zhangcheng",
};
obj.__proto__ = {
message: "hello aaa",
};
obj.__proto__.__proto__ = {
message: "hello bbb",
};
obj.__proto__.__proto__.__proto__ = {
message: "hello ccc",
};
console.log(obj.message);
通过把对象的隐式原型改造,让其一层一层的去查找,这样就形成了原型链
同时这就是继承的思想,我们可以创建一个父类,创建的子类去 继承父类,子类实例出来的对象,在查找值的时候,首先在自身查找,之后会顺着原型链,去子类查找,子类若没有,就会查找父类,最后会查找到 Object
的原型对象—>null
既然了解了理论,那么我们就开始使用原型链实现继承(ES5),有可能在使用定义变量用的ES6的语法,但是思想是最重要的
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log("running");
};
function Student(name, age, classroom) {
this.name = name;
this.age = age;
this.classroom = classroom;
}
Student.prototype.running = function () {
console.log("running");
};
Student.prototype.study = function () {
console.log("studying");
};
Student
类添加方法的时候,实际上是给Person类添加的方法(详见内存图)function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log("running");
};
//方式一:将Student的显式原型直接指向Peoson的显式原型
//或者将stu1的隐式原型指向Peoson的显式原型
Student.prototype = Person.prototype;
function Student(name, age, classroom) {
this.name = name;
this.age = age;
this.classroom = classroom;
}
//Student.prototype.running = function () {
// console.log("running");
//};
Student.prototype.study = function () {
console.log("studying");
};
let stu1 = new Student("zhangcheng", 18, 2002);
stu1.running();
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log("running");
};
//方式一:pass
// Student.prototype = Person.prototype;
//借用第三方,实现方法继承
let p = new Person();
Student.prototype = p;
function Student(name, age, classroom) {
//借用构造函数的方式实现属性继承
Person.call(this, name, age);
this.classroom = classroom;
}
// Student.prototype.running = function () {
// console.log("running");
// };
Student.prototype.study = function () {
console.log("studying");
};
let stu1 = new Student("zhangcheng", 18, 2002);
console.log(stu1.name, stu1.age, stu1.classroom);
stu1.running();
stu1.study();
是最终的解决方案
这种思想是道格拉斯·克罗克福德提出来的
为了理解这种思想,我们需要回顾一下上面为了实现方法继承的目的
因此为了 满足以上条件,且 不出现两份父类的元素,就出现了以下代码
//传入的o是父类的显式原型对象
function F(o){
//让该函数的显式原型,指向父类的显式原型
F.prototype = o
//返回一个F类的对象,这个对象的隐式原型--->F类的显式原型--->o的显式原型
return new F()
}
function Person(){
}
function Student(){
}
//这样就将Student的显式原型指向了新创建出来的对象
Student.prototype = F(Person.prototype)
//这样的继承方式,就不会有两份父类的属性
通过以上思想,也可以使用Object.create()
来创建
Object.create()方法创建的对象,需要手动指定该对象的隐式原型指向哪里
以下是实现的具体代码
function inherit(Subtype, Supertype) {
//将子类的显式原型对象,指向新创建的对象
//该对象的隐式原型对象,指向的是父类的显式原型对象
Subtype.prototype = Object.create(Supertype.prototype);
//给子类的显式原型对象,设置constructor
Object.defineProperty(Subtype.prototype, "construtor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype,
});
}
//创建父类
function Person(name, age) {
this.name = name;
this.age = age;
}
//给父类添加方法
Person.prototype.running = function () {
console.log("running");
};
//创建子类
function Student(name, age, classroom) {
//借用构造函数的方式实现属性继承
Person.call(this, name, age);
this.classroom = classroom;
}
//使子类继承父类
inherit(Student, Person);
//给子类添加方法
//一定要写在继承之后!!
Student.prototype.study = function () {
console.log("studying");
};
let stu1 = new Student("zhangcheng", 18, 2002);
console.log(stu1.name, stu1.age, stu1.classroom);
stu1.running();
console.log(stu1);
stu1.study();
函数对象最终也是继承自Object
Object.prototype.message = "zhangcheng"
function foo(){}
foo.message//zhangcheng
所以,我们创建出来的对象,可以使用toString等方法,是因为Object上面有这些方法
prototype
);以对象角度(每个函数都可看成一个对象)有相应的 隐式原型(__proto__
)new Function()
方式创建)
//构造函数Person
function Person(name) {
this.name = name;
}
//只能通过p1调用:实例方法
Person.prototype.study = function () {
console.log("123");
};
//只能通过Person进行调用:类方法
Person.running = function () {
console.log("456");
};
let p1 = new Person();
p1.study();
Person.running();
判断某个属性,是否属于对象本身的(不是在原型上的属性)
function Person(name){
this.name = name
}
function Student(className){
this.className = className
}
//Student 继承自 Person
let stu1 = new Student(2002)
stu1.hasOwnProperty("className")//true
stu1.hasOwnProperty("name")//false
判断某个属性是否在某个对象上,或者原型上
function Person(name){
this.name = name
}
function Student(className){
this.className = className
}
//Student 继承自 Person
let stu1 = new Student(2002)
console.log("name" in stu1)//true
console.log("className" in stu1)//true
//注意,for in 遍历对象的时候,会将原型链上出现的属性都遍历出来
for(let key in stu1){
console.log(key)
}
用于检测 构造函数的显式原型,是否出现在 某个实例对象原型链上
也可以大致理解为,某个实例对象,是否是该构造函数创造出来的
这个原理就是
function Person(name){
this.name = name
}
function Student(className){
this.className = className
}
//Student 继承自 Person
let stu1 = new Student(2002)
console.log(stu1 instanceof Student)//true
console.log(stu1 instanceof Person)//true