JS面向对象(二)-原型、原型链

187 阅读3分钟

一、new操作符调用的作用

  1. 在内存中创建一个新的空对象
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
  3. 构造函数内部的this,会指向创建出来的新对象
  4. 执行函数的内部代码(函数体代码)
  5. 如果构造函数没用返回非空对象,则返回创建出来的新对象

二、原型

  • Object.prototype是所有类的父类
  • 每个对象都有个原型__proto__(隐式原型)
  • 每个函数都有一个原型prototype(显示原型),同时也有__proto__隐式原型(函数也是特殊的对象)
function Foo(){

}

//隐式原型(有浏览器兼容问题)
console.log(Foo.__proto__);    //{}

//显示原型(无浏览器兼容问题)
console.log(Foo.prototype);    //{}

let f1 = new Foo();
let f2 = new Foo();
console.log(f1.__proto__ === Foo.prototype);  //true
console.log(f2.__proto__ === Foo.prototype);  //true

三、定义在构造函数内的方法和定义在prototype原型上方法的区别

  1. 方法所处位置不同,构造函数内方法是定义在构造函数内,而prototype上的方法是定义在原型链上
 // 构造函数A
function A(name) {
    this.name = name;
    this.sayHello = function () {
        console.log("Hello, my name is: " + this.name);
    };
}

// 构造函数B
function B(name) {
    this.name = name;
}
B.prototype.sayHello = function () {
    console.log("Hello, my name is: " + this.name);
};
  1. 使用函数内的方法我们可以 访问到函数内部的私有变量,如果我们通过构造函数 new 出来的对象需要我们操作构造函数内部的私有变量的话,这个时候就要考虑使用函数内的方法
let a1 = new A('这是a1')
let b1 = new B('这是b1')
console.log(a1);
console.log(b1);

image.png

  1. 如果需要通过构造函数创建大量的对象的话,且该对象有许多方法时,用函数内部定义的话会导致占用内存过大,这时挂载在prototype原型上是个优质的选择

四、原型链

每个对象都有一个指向它的原型(prototype)对象的内部链接。每个原型对象又有自己的原型,直到某个对象的原型为null为止,组成这条链的最后一环

let obj = {
    name: 'why',
    age: 18
}
obj.__proto__ = {

}
obj.__proto__.__proto__ = {

}
obj.__proto__.__proto__.__proto__ = {
    fathAddress: '9988'
}
console.log(obj.fathAddress);  //9988

image.png

  • 示例代码图
let obj1 = {
    name: 'why',
    age: 18
}

let obj2 = {
    address: '深圳市'
}

obj1.__proto__ = obj2

image.png

五、继承

通过【某种方式】让一个对象可以访问到另一个对象中的属性和方法

  • 原型链继承(通过new父类的对象作为子类的原型,通过原型链实现继承)
//父类:公共属性和方法
function Person(name){
    this.name = name
}

Person.prototype.eating = function(){
    console.log(this.name + 'eating');
}

//子类:特有属性和方法
function Student(){
    this.sno = 111
}

//子类继承父类
Student.prototype = new Person('hjh');

Student.prototype.studying = function(){
    console.log(this.name + 'studying');
}

let stu = new Student();
console.log(stu);
  • 弊端:
  • 打印stu,是通过原型链继承,继承的属性是看不到的,只会打印stu对象内可枚举属性,

image.png

  • 共享原型会导致数据被改变
//父类:公共属性和方法
function Person(){
    this.name = 'hjh',
    this.friends = []
}

Person.prototype.eating = function(){
    console.log(this.name + 'eating');
}

//子类:特有属性和方法
function Student(){
    this.sno = 111
}

//子类继承父类
Student.prototype = new Person();

Student.prototype.studying = function(){
    console.log(this.name + 'studying');
}

let stu1 = new Student();
let stu2 = new Student();

console.log(stu1);
stu1.friends.push('why')    //获取引用,修改引用中的值,会相互影响
stu1.name = 'newName'       //直接修改对象上的属性,是给本对象添加了一个新属性,是不会相互影响的
console.log(stu2);    //此时stu2.friends也随着改变
  • 实例化构造函数时不好传参,将name参数传递给父类而不是让子类接收

  • 借用构造函数方案实现继承,解决上述原型链继承所有弊端

1132b9b439d44f4439eb29b673762af.jpg

//父类:公共属性和方法
function Person(name, age, friends){
    //此时this指向Student对象
    this.name = name,
    this.age = age,
    this.friends = friends
}

Person.prototype.eating = function(){
    console.log(this.name + 'eating');
}

//子类:特有属性和方法
function Student(name, age, friends, sno){

    //通过call将Student的this及其他公共属性需要在父类调用的传入
    Person.call(this, name, age, friends)

    this.sno = sno
}

//子类继承父类
Student.prototype = new Person();

Student.prototype.studying = function(){
    console.log(this.name + 'studying');
}

let stu1 = new Student('student1',18,['张三'],182);
stu1.friends.push('李四')
console.log(stu1);
let stu2 = new Student('student2',21,['李四'],176);
console.log(stu2);

image.png

  • 借用构造函数方案实现继承弊端:(移步至js面向对象三有解决方案)
  • 父类Person函数会被调用多次
  • stu的原型对象上会多出一些属性,但是这些属性是没有必要的存在

image.png