思维导图
什么是构造函数
JavaScript语言使用构造函数作为对象的模板,描述实例对象的基本结构,,实例对象的属性和方法,可以定义在构造函数内部,一个构造函数,可以生成多个实例对象。其实构造函数本身就是一个普通的函数,为了区分普通函数,我们通常将构造函数的首字母大写。
function Person(name) {
this.name = name;
this.sayHi = function () {
console.log('大家好,我是' + this.name);
};
this.walk = function() {
console.log("walk");
};
}
let person1 = new Person(‘张宇’);
let person2 = new Person('张三');
person1.sayHi(); //大家好,我是张宇
person2.sayHi(); //大家好,我是张三
上述代码中,Person 就是一个构造函数,同时我们使用new 方法生成了两个实例对象 person1 和 person2
prototype
由于person1 和 person2 都是由同一个构造函数的两个实例, 因此它们拥有构造函数Person 的属性和方法
person1.walk(); //walk
person2.walk(); //walk
console.log(person1.walk === person2.walk); //false
上述代码可以看出来,person1 和 person2 都拥有一个相同的方法walk , 但却不是同一个方法,也就是说,每创建一个实例,都会在内存中新建一个walk方法 ,这样会十分浪费系统资源,因为所有的walk方法都是一样的,应该完全共享。
这时候就到了 prototype出场的时候了,那么什么是prototype?
JavaScript 语言规定,每个函数都会有一个 prototype 属性,指向一个对象
console.dir(Person);
打印构造函数Person 时发现确实有prototype属性,当构造函数生成实例时。prototype属性会自动成为实例对象的原型,JavaScript 继承机制的设计思想是,原型对象的所有属性和方法,都能被实例对象继承,也就是说,如果将对象的属性和方法定义在原型上,就能解决上述问题,从而节省系统资源。
function Person(name) {
this.name = name;
}
Person.prototype.walk = function() {
console.log("walk");
}
let person1 = new Person("张宇");
let person2 = new Person("张三");
console.log(person1.hasOwnProperty('walk')); //false
person1.walk(); //walk
person2.walk(); //walk
console.log(person1.walk === person2.walk); //true
上述代码中,通过hasOwnProperty0方法可以看出,person1本身并没有walk()方法 ,因为walk是添加在原型对象的方法,所有的实例对象都共享了该方法。原型对象的属性和方法不是实例对象的属性和方法
但如果实例对象本身就拥有这个属性或方法,那么就不会再去原型对象寻找这个属性和方法
person1.walk = function () {
console.log('run')
}
person1.walk(); //run
上述代码中可以看出,当我给实例对象person1添加了walk方法后,他就不再去原型对象中寻找这个方法了。也就是说,只有当实例对象本身没有某个属性或者方法的时候,它才会去原型对象寻找某个属性和方法。
当我们打印Person.prototype时,可以看出他有constructor属性和__proto__属性
console.dir(Person.prototype);
constructor
这个属性默认指向prototype对象所在的构造函数
console.log(Person.prototype.constructor === Person); //true
console.log(person1.constructor === Person); //true
console.log(person1.hasOwnProperty('constructor')); //false
由于constructor是定义在prototype对象上面的属性,因此每一个实例对象都可以调用,我们在实例对象上调用该属性,其实等同于prototype对象直接调用
正是因为这个特性,我们可以实现从一个实例对象上创建一个新的实例对象
let person3 = new person1.constructor('李四');
console.log(person3.name); //李四·
constructor属性表示的是prototype对象与构造函数之间的关系,因此当修改原型对象是,最好将同时修改constructor属性,不然会照成一些不必要的问题出现
Person.prototype = {
swim: function() {
console.log('swim');
}
}
let person4 = new Person('王五');
person4.swim(); //swim
// person1.swim(); //person1.swim is not a function
//由于我们直接修改了Person.prototype,修改前的实例对象则还指向原对象,因此实例person1无法访问到 swim方法,
console.log(person1.__proto__ === person4.__proto__); //false
上述代码我们可以发现,person1和person4的原型对象不是指向同一个。
因此,我们最好只在原型对象中添加属性方法,而不是直接进行修改。
__proto__
每个实例对象都有__proto__属性,该属性返回对象原型。
function Person(name) {
this.name = name;
}
let person = new Person('张宇');
console.log(person.__proto__ === Person.prototype); //true
MDN是这样解释它的
Object.prototype的__proto__属性是一个访问器属性(一个getter函数和一个setter函数), 暴露了通过它访问的对象的内部[[Prototype]](一个对象或null)。使用
__proto__是有争议的,也不鼓励使用它。因为它从来没有被包括在EcmaScript语言规范中,但是现代浏览器都实现了它。__proto__属性已在ECMAScript 6语言规范中标准化,用于确保Web浏览器的兼容性,因此它未来将被支持。它已被不推荐使用, 现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf和Object.setPrototypeOf/Reflect.setPrototypeOf(尽管如此,设置对象的[[Prototype]]是一个缓慢的操作,如果性能是一个问题,应该避免)。proto 属性也可以在对象文字定义中使用对象[[Prototype]]来创建,作为
Object.create()的一个替代。
原型链
JavaScript 规定,每个对象都有自己的原型,且可以充当其他对象的原型。
对象的原型也是对象,那么它也会有自己的原型,这样就会形成一条“原型链”,对象---原型---原型的原型----
所有的原型对象最终都可以追溯到Object.prototype对象,而Object.prototype也是个对象,那它的原型是什么?
console.log(Object.prototype.__proto__); //null
上述代码得知,Object.prototype对象的原型是 null,null 没有自己的原型,也没有任何属性和方法,因此,原型链的尽头就是null。
之前内容我们得知,当我们读取对象的属性和方法时,会先在自身读查找,如果没有就到其原型查找,如果还是没有,则到其原型的原型查找,直到Object.prototype对象,如果还是找不到,则返回undefined,因此,我们得知,所有的对象都会继承Object.prototype对象的属性和方法,如果对象和其原型都定义了一个同名属性或方法,则优先读取对象自身的属性和方法,覆盖掉其原型的属性和方法。
根据上述内容我们可以的到以上关系图(其中橙色线为原型链)
之前我们说过,构造函数本质上也是一个函数,那么它是不是也有着自己的原型?
console.log(Person.__proto__ === Function.prototype); //true
//Function.prototype 也是对象
console.log(Function.prototype.__proto__ === Object.prototype); //true
根据上述内容我们又可以得到以下关系图,两者结合就是完整的关系图