- 说明:该文属于 大前端全栈架构白宝书专栏,目前阶段免费,如需要项目实战或者是体系化资源,文末名片加V!
- 作者:哈哥撩编程,十余年工作经验, 从事过全栈研发、产品经理等工作,目前在公司担任研发部门CTO。
- 荣誉:2022年度博客之星Top4、2023年度超级个体得主、谷歌与亚马逊开发者大会特约speaker、全栈领域优质创作者。
- 🏆 白宝书系列
原型和原型链是实现JavaScript继承的主要机制,许多面向对象的特性,如类的方法继承等,都是基于原型和原型链的。
在JavaScript中,每个对象都有一个特殊的内部属性[[Prototype]],它是对象的原型。原型也就是其他对象的引用,每个对象都从原型“继承”属性。
原型链是通过同样的[[Prototype]]属性链接起来的,形成了一条链状结构。“原型链”就是对象通过[[Prototype]]属性引用其他对象,然后通过这些对象再引用其他对象,如此形成的一条链状结构。
当我们访问对象的某个属性时,JavaScript会首先在该对象自身的属性中查找,如果没有找到,那么JavaScript会在该对象的[[Prototype]]原型对象中查找,如果还是没有找到,那么JavaScript就会继续在原型的原型中查找,即在原型链上向上查找,直到找到属性或者查找到null(原型链的末端)为止。如果在原型链的最顶端也没有找到这个属性,那么就会返回undefined。这就是原型链在JavaScript属性查找中的作用。
prototype,原型。任何函数都有prototype属性。
prototype属性值是个对象,它默认拥有constructor属性指回函数
下面我们来敲一个例子证明prototype
这个属性的存在,再看一下它的具体功能:
function sum(a, b) {
return a + b;
}
console.log(sum.prototype);
console.log(typeof sum.prototype); // object
console.log(sum.prototype.constructor);
console.log(sum.prototype.constructor === sum); // true;证明protoype的constructor属性指向原函数
- 对于普通函数来说,prototype属性没有任何用处,而构造函数的prototype属性非常有用
- 构造函数的prototype属性是它的实例的原型
下面我们看下一张图来解释什么叫做“构造函数的prototype属性是它的实例的原型”
乍一看这个图并不能理解其中的含义,只是知道构造函数、构造函数的prototype属性和构造函数的实例三者之间存在一种“三角”关系。我们来敲一下代码,从代码维度来理解这三者的关系:
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 实例化
var xiaoming = new People('小明', 12, '男');
// 测试三角关系是否存在
console.log(xiaoming.__proto__ === People.prototype); // true
上面的代码只是证明了构造函数、构造函数的prototype属性和构造函数的实例存在的这种三角关系,但这个关系到底在编写代码中有什么作用呢?答案是,我们可以利用这个三角关系实现“原型链查找”。
JavaScript规定:实例可以打点访问它的原型的属性和方法,这被成为“原型链查找”
示例代码:
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 往原型上添加nationality属性
People.prototype.nationality = '中国';
var xiaoming = new People('小明', 12, '男');
console.log(xiaoming.nationality);
console.log(xiaoming);
当实例化对象打点调用某个属性时,系统发现它本身没有这个属性,此时并不会报错,而是回去查找它的“原型”上有没有这个属性,如果有,则会把这个属性输出。
下面把上面的例子延伸一下,我们在People这个构造函数再实例化一个对象,这个对象本身就有nationality属性,那我们用这个对象打点调用nationality属性时,会返回什么呢?示例代码:
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 往原型上添加nationality属性
People.prototype.nationality = '中国';
// 实例化
var xiaoming = new People('小明', 12, '男');
var tom = new People('Tom', 11, '男');
tom.nationality = '美国';
console.log(xiaoming.nationality);
console.log(xiaoming);
console.log(tom.nationality); // 美国
可以看到,如果这个实例化对象本身就有nationality属性时,打点会直接访问到这个属性,而不是去访问原型上的nationality属性,这个就是原型链的遮蔽效应
。
hasOwnProperty方法可以检查对象是否真正“自己拥有”某属性或者方法
和hasOwnProperty方法有些类似的是in
运算符:
in运算符只能检查某个属性或方法是否可以被对象访问,不能检查是否是自己的属性或方法
示例代码:
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 往原型上添加nationality属性
People.prototype.nationality = '中国';
// 实例化
var xiaoming = new People('小明', 12, '男');
console.log(xiaoming.hasOwnProperty('name')); // true
console.log(xiaoming.hasOwnProperty('age')); // true
console.log(xiaoming.hasOwnProperty('sex')); // true
console.log(xiaoming.hasOwnProperty('nationality')); // false
console.log('name' in xiaoming); // true
console.log('age' in xiaoming); // true
console.log('sex' in xiaoming); // true
console.log('nationality' in xiaoming); // true
之前在实例化对象时,是直接往实例化对象上添加方法,实例化多少个对象,就会在内存中创建多少个方法,这些方法虽然名字相同,但在内存中的地址是不同的,敲一段代码来验证一下:
function People(name) {
this.name = name;
this.sayHello = function () {
};
}
// 实例化
var xiaoming = new People('xiaoming');
var xiaobai = new People('xiaobai');
var xiaohei = new People('xiaohei');
console.log(xiaoming.sayHello === xiaobai.sayHello);
把方法直接添加到实例身上的缺点:每个实例和每个实例的方法函数都是内存中不同的函数,造成了内存的浪费
如果把方法添加到prototype身上,就可以避免这个问题,将上面的例子改造后代码如下:
function People(name) {
this.name = name;
}
//把方法写到原型上
People.prototype.sayHello = function () {
console.log('您好,我是' + this.name);
}
// 实例化
var xiaoming = new People('xiaoming');
var xiaobai = new People('xiaobai');
var xiaohei = new People('xiaohei');
xiaoming.sayHello();
xiaobai.sayHello();
xiaohei.sayHello();
console.log(xiaoming.sayHello === xiaobai.sayHello);
把方法写在原型上是聪明有效的避免内存重复占用的方式,我们一定要习惯使用这种写法。