原型、原型链与继承
构造函数
构造函数创建实例的过程
1.创建一个新对象
2.将空对象的__proto__指向构造函数的原型
3.修改构造函数中this指向,将构造函数中的this指向实例对象,执行构造函数中的代码,给这个新对象添加属性和方法(通过call/apply)
4.返回新对象(实例对象)
手写new
function Person(name, age) {
this.name = name;
this.age = age;
}
// const p = new Person("张三", 40);
// console.log(p);
function myNew(Fn, args) {
let obj = {};
obj.__proto__ = Fn.prototype;
Fn.apply(obj, args);
return obj;
}
const p = myNew(Person, ["张三", 40]);
console.log(p);
原型
原型与原型链都都源于对象并服务于对象,他们是js实现继承的一种模型
原型:每个构造函数都有一个prototype属性,它就是通过构造函数创建(new)的对象的原型,在他上面定义的属性和方法都可以被对象实例所共享
function Fn(name) {
this.name = name;
this.speak = function () {
console.log("Chinese");
};
}
Fn.saySomething = function () {
console.log("i love you");
};
const fn = new Fn("张三");
fn.speak(); // Chinese
fn.saySomething(); // 报错 saySomething is not a function
// 但是如果我将saySomething放到Fn的prototype中,所有实例都可以使用这个方法
function Fn(name) {
this.name = name;
}
Fn.prototype.saySomething = function () {
console.log("i love you");
};
const fn = new Fn("张三");
const fn2 = new Fn("李四");
fn.saySomething(); // i love you
fn2.saySomething(); // i love you
protype对象
1>proto
属性
js中,除去null外任何对象内部都会自带__proto__
属性;prototype是一个对象,所以存在__proto__
属性
fn.__proto__==>Fn.prototype
2>constructor属性
对象的的prototype里面有个constructor属性,指向当前对象所属的构造函数
Fn.prototype.constructor==>Fn构造函数
每个构造函数都有一个prototype
属性,指向原型对象,原型对象上有个constructor
属性指回构造函数,每个实例对象都有一个__proto
__属性,指向构造函数的prototype
原型链
原型链:每个对象都有一个__proto__
属性,指向他的原型,也就是构造函数的prototype;当访问一个对象的属性时,它首先会在自己身上找,如果没有找到就会往原型上面找,如果还是没找到,他会继续往上,直到找到为止,如果查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined
,这样就会形成一条链,就是原型链
原型链的终点是null,Object.prototype .__proto__
指向null
继承
继承的本质是重写原型对象
原型链继承
可以继承属性和方法
function Boy() {
this.gender = "male";
}
function Girl() {
this.gender = "female";
this.color = "pink";
}
Girl.prototype.getColor = function () {
return this.gender;
};
Girl.prototype.getGender = function () {
return this.gender;
};
// 创建Girl实例,并加那个该实例赋值给Boy原型
Boy.prototype = new Girl();
const baby = new Boy();
console.log(baby.getColor()); // pink
console.log(baby.getGender()); // male
缺点:
-
多个实例对引用类型的操作会被篡改
-
在创建子类型是不能向超类型的构造函数中传参
function Boy() { this.colors = ["blue", "green"]; } function Girl() {} Girl.prototype = new Boy(); const baby1 = new Girl(); baby1.colors.push("black"); console.log(baby1.colors); //["blue", "green","black"] // baby2和baby1的构造函数一样,都到原型上找,指向一致,color是引用类型,所以baby2也跟着变了 const baby2 = new Girl(); console.log(baby2.colors); //["blue", "green","black"]
构造函数继承
通过call()、apply()来实现继承
call
apply
缺点:只能继承父类的实例属性和方法,无法继承原型属性、方法
function Boy() {
this.gender = "male";
// 继承Boy
Girl.call(this);
}
function Girl() {
this.gender = "female";
this.color = "pink";
}
Girl.prototype.getGender = function () {
return this.gender;
};
const baby = new Boy();
console.log(baby); // {gender:"female",color:"pink"}
console.log(baby.getGender()); //报错: baby.getGender is not a function
组合继承
使用原型链实现原型属性和方法的继承,通过构造函数实现对实例属性的及继承
function Boy(name) {
this.name = name;
this.colors = ["blue"];
}
Boy.prototype.getName = function () {
return this.name;
};
function Girl(name, age) {
// 先利用构造函数继承来继承实例对象的属性和方法
Boy.call(this, name);
this.age = age;
}
// 在利用原型继承来继承原型
Girl.prototype = new Boy();
const baby1 = new Girl("baby1", 0);
const baby2 = new Girl("baby2", 1);
console.log(baby1);
console.log(baby1.getName()); // baby1
console.log(baby2.getName()); // baby2
baby1.colors.push("pink");
// 实例自身已经有colors属性,就不会到原型上找,所以不会相互影响
console.log(baby1.colors); // ['blue','pink']
console.log(baby2.colors); // ['blue']
组合继承融合了两者的有点,避免了他们的缺陷
原型式继承
object()对传入其中的对象执行了一次浅复制
,将构造函数F的原型直接指向传入的对象
function object(obj) {
function F() {}
F.prototype = obj;
return new F();
}
const person = {
name: "test",
colors: ["blue"],
};
const p = object(person);
p.name = "hello";
p.colors.push("pink");
console.log(p.colors); // ["blue","pink"]
const p2 = object(person);
p2.name = "world";
p2.colors.push("white");
console.log(p2.colors); // ["blue","pink",'white']
console.log(p.colors); // ["blue","pink",'white']
缺点:
- 多个实例对引用类型的操作会被篡改
- 在创建子类型是不能向超类型的构造函数中传参
寄生式继承
在原型的基础上,增强对象,返回构造函数
function object(obj) {
function F() {}
F.prototype = obj;
return new F();
}
function objectAnother(origin) {
const clone = object(origin);
clone.greet = function () {
alert("hello");
};
return clone;
}
const person = {
name: "test",
colors: ["blue"],
};
const p = objectAnother(person);
p.colors.push("pink");
p.greet();
console.log(p.colors); // ['blue',pink]
const p2 = objectAnother(person);
p2.colors.push("white");
console.log(p2.colors); // ["blue","pink",'white']
console.log(p.colors); // ["blue","pink",'white']
寄生组合式继承
function inheritPrototype(subType, superType){
const prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
// 新增子类原型属性
SubType.prototype.sayAge = function(){
alert(this.age);
}
const instance1 = new SubType("xyc", 23);
const instance2 = new SubType("lxy", 23);
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]
ES6类继承extends
ES6支持类的继承,它背后依旧是使用原型链
class Person {
static myStaticProp = 42; // 静态属性:class本身的属性,不是定义到实例对象(this)上面的属性
constructor(name, ageNum) {
this.name = name;
this.age = ageNum;
}
getDoubleAge() {
return 2 * this.age;
}
}
const p = new Person("潘周聃", 29);
console.log(p.getDoubleAge()); // 58
class personalInfo extends Person {
constructor(name, age, info) {
super(name, age); // 不能在调用super之前引用this
this.info = info;
}
getInfo() {
return this.info;
}
}
const p2 = new personalInfo("潘周聃", 29, "硕士毕业于苏黎世联邦理工大学");
console.log(p2.getDoubleAge()); // 58
console.log(p2.getInfo()); //"硕士毕业于苏黎世联邦理工大学"
扩展
instanceof
基本语法
返回布尔值
const arr = [1];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
console.log(null instanceof Object); // false
原理
右侧的对象(构造函数)的原型对象prototype)是不是在左侧对象的原型链上
手写instanceof
function myInstLnce(L, R) {
if (typeof L !== "object" || L === null) return false;
const origin = R.prototype;
L = Object.getPrototypeOf(L);
while (true) {
if (L === origin) return true;
L = Object.getPrototypeOf(L);
}
}
apply call bind
call、apply、bind的区别
都可以改变this的指向
1》call 和 apply 改变this指向的同时,会调用函数,bind改变函数的this指向,不会调用
2》call 和apply 的传参不同,第一个参数都是this指向的执行上文,后面的参数都是作为改变this指向的函数的参数;call 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上,apply第二个参数必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上
function fn(uname, age) {
this.name = uname;
this.age = age;
}
const obj = {
name: "张三",
age: 20,
};
fn.call(obj, "test", 22);
console.log(obj);
fn.apply(obj, ["测试", 33]);
console.log(obj);
3》在使用上的区别:
call:对象的继承,在子构造函数这种调用父构造函数,但是改变this指向,就可以继承父的属性
function superClass () {
this.a = 1;
this.print = function () {
console.log(this.a);
}
}
function subClass () {
superClass.call(this);
this.print();
}
subClass(); // 1
apply的应用场景: Math.max,获取数组中最大、最小的一项
const max = Math.max.apply(null, array) // 和Math.max(...array)效果一样
第一个参数,是一个对象。 函数的调用者,将会指向这个对象。如果不传,则默认为全局对象 window
注意点:
多次 bind 时只认第一次 bind 的值
箭头函数中this不会被改变