什么是构造函数
任何函数都可以作为构造函数。当该函数通过new关键字调用的时候,我们就称之为构造函数。
原型和原型链
-
所有对象都是通过
new
函数创建 -
所有的函数也是对象,函数中可以有属性
-
所有对象都是引用类型
原型 prototype
-
所有函数都有的属性 prototype 称为函数原型。作用:让某一个构造函数实例化的所有对象可以找到公共的方法和属性。
-
默认情况下,prototype 是个普通 object 对象
-
prototype 中有 constructor 属性,它也是一个对象,表示当前对象的构造函数。
隐式原型 __proto__
属性
-
__proto__
属性是对象特有的属性,它表示当前对象的原型对象是谁。 -
默认情况下,对象的
__proto__
属性就是它的构造函数的 prototype 属性。
当访问一个对象的成员时
-
看该对象自身是否拥有该成员,如果有直接使用
-
看该对象的隐式原型是否拥有该成员,如果有直接使用
猴子补丁:在函数原型中加入成员,以增强起对象的功能。猴子补丁会导致原型污染,使用需要谨慎
原型链
每个对象都有一个原型(prototype),并从原型上继承属性和方法,原型本身也是一个对象,它也有自己的原型,形成一个链式结构。这种链式结构就被称为原型链。
特殊点
-
Function 的
__proto__
属性指向 Function -
Object.prototype 的
__proto__
指向 null
继承(原型链继承、构造函数继承、组合继承、寄生组合继承)
原型链继承
每个构造函数都有一个原型对象(prototype
),每个实例对象包含一个指向原型对象的指针(_proto_
)。这样,每当代码读取实例的某个属性时,都会首先在实例上搜索这个属性,如果没有找到,则会搜索原型对象。
缺点
原型链继承的一个主要问题是包含引用类型值的原型属性会被所有实例共享。 换而言之,如果一个实例改变了该属性,那么其他实例的该属性也会被改变
// 定义父类
function Parent() {
this.name = "parent";
this.sayHello = function () {
console.log("Hello");
};
}
// 在父类的原型上定义方法
Parent.prototype.getName = function () {
return this.name;
};
// 定义子类
function Child() {
//console.log(new.target)
this.name = "child";
}
// 子类继承父类,这里是关键,实现原型链继承
Child.prototype = new Parent();
// 实例化子类
var child1 = new Child();
var child2 = new Child();
console.log(child1.sayHello === child2.sayHello); //true
console.log(child1.getName()); //child
构造函数继承
构造函数继承,通过使用 call 或 apply 方法,我们可以在子类型构造函数中执行父类型构造函数,从而实现继承。 这种继承方式的好处是,原型属性不会被共享(所以不会出现上述问题)。 那么这种方式的缺点是什么呢?它不能继承父类 prototype 上的属性。
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
function Child() {
Parent.call(this); //构造函数继承
}
Parent.prototype.a = "parent的属性a";
var child1 = new Child();
var child2 = new Child();
console.log(child1.sayHello === child2.sayHello); // false
console.log(child1.a); //undefined
组合继承
组合继承可以理解为 原型链继承 + 构造函数继承。优化了构造函数继承的缺点,实现继承父类 prototype 上的属性。
优点
-
原型属性不会被共享
-
可以继承父类原型链上的属性和方法
但是也有缺点
-
调用了 2 次 Parent()。
-
它在 child 的 prototype 上添加了父类的属性和方法。
child1 是一个子类对象,这个对象本身有 sayHello 方法。而 Child.prototype 上也有一个 sayHello 方法。
-
为什么 child1 本身有 sayHello 方法?因为在 Child() 构造函数内部,我们调用了一次 Parent(), 这就给 child1 对象添加了 sayHello 方法。
-
为什么 Child.prototype 上也有一个 sayHello 方法?因为 Child.prototype = new Parent();
我们每次调用 child1.sayHello() 的时候,永远是调用的 child1 本身的 sayHello 方法。换句话说,Child.prototype 上 sayHello 方法我们根本不需要。
寄生组合继承
只需要在组合继承的基础上,把 Child.prototype = new Parent(); 替换为 Child.prototype = Object.create(Parent.prototype);
这两者有什么区别呢? Object.create(Parent.prototype) 创造了一个空对象,这个空对象的__proto__ 是 Parent.prototype 。所以它继承了 Parent 原型链上的属性和方法。由于我们删除了 Child.prototype = new Parent(); 我们不再调用 Parent() 构造函数,因此 Child.prototype 不再包含 Parent 的属性和方法。Child.prototype 上不再有 sayHello 方法。
寄生组合继承的缺点就是,Child.prototype 的原始属性和方法会丢失。
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
function Child() {
Parent.call(this);
}
Parent.prototype.a = "parent的属性a";
Child.prototype = Object.create(Parent.prototype); // 寄生组合继承
Child.prototype.constructor = Child; // 寄生组合继承
var child1 = new Child();
var child2 = new Child();
console.log(child1.sayHello === child2.sayHello); // false
console.log(child1.a); //parent的属性a
面试
使用原型链的知识,完成一道简单的笔试题
// 写出obj的构造函数的名称
function create() {
if (Math.random() < 0.5) {
return {};
} else {
return [];
}
}
var obj = create();
console.log(obj.__proto__.constructor.name);
Function.prototype.a = () => {
console.log(1);
};
Object.prototype.b = () => {
console.log(2);
};
function A() {}
const a = new A();
a.a(); // TypeError: a.a is not a function
a.b(); // 2
A.a(); // 1
A.b(); // 2
new()和 Object.create() 区别
-
new 操作符用于创建一个给定构造函数的实例对象,实例可以访问到构造函数中的属性和构造函数原型链中的属性
-
Object.create 创建一个新对象(没有 construactor 属性), 其隐式原型指向新对象。
如何区分普通函数和构造函数
-
通过 class 定义构造函数,默认 new 方式创建实例
-
构造函数默认的返回值是创建的对象(也就是实例),普通函数的返回值由 return 语句决定
-
可以通过函数内部的
new.target
属性判断(普通函数 new.target 为空)
new.target 属性只能在函数内部使用
原型的作用是什么
原型是 JS 实现面向对象的手段之一,原型的存在避免了类型丢失