JS重难点之一:原型链和继承

1 原型解决的问题

在总结原型之前,先简单回顾一下较为简单常用的创建对象的方式。

1.1 对象字面量和new Object()

对象字面量创建方式:

var person1 = {
    name: "Li Xiaoming",
    age: 18,
    id: 1,
    sayName: function(){
        console.log(this.name)
    }
}
 
 person1.sayName()     //"Li Xiaoming"

new Object()方式:

var person2 = new Object();
person2.name = "Ma DongMei";
person2.age = 19;
person2.id = 2;
person2.sayName = function(){
    console.log(this.name)
}
person2.sayName()     //
"Ma DongMei"

上面这两种创建对象的方式有如下缺点:

1.当需要创建大量类似的person时,会产生很多冗余代码

2.对象一直都是Object类,没有解决对象识别的问题

1.2 工厂模式

工厂模式就是封装一个创建对象的工厂(函数),将创建对象的细节放在这个函数中处理,每次需要对象的时候,调用该函数,它会生产(返回)一个对象实例。

function createPerson(name, age, id){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.id = id;
    o.sayName = function(){
        console.log(this.name);
    }
    return o;
}

var person3 = createPerson("Xia Lou", 20, 3);
person3.sayName()   //"Xia Lou"

缺点:

虽然解决了代码冗余问题,但是任然没有解决对象识别的问题,工厂模式创建的对象任然都是Object类

1.3 构造函数模式

使用构造函数模式创建对象依赖new运算符(构造函数的本质任然是函数,其实也可以直接调用,待会再说这个问题)。

1.3.1 构造函数和工厂模式的区别

使用构造函数创建实例的过程如下:

function Person(name, age, id){
    this.name = name;
    this.age = age;
    this.id = id;
    this.sayName = function(){
        console.log(this.name)
    }
}

var person4 = new Person("Da Zhuang", 21, 4);
person4.sayName();    //"Da Zhuang"

可以看到,它与工厂模式有如下几个区别

1.没有显示的创建对象

2.直接将属性和方法赋给了this

3.没有return语句

4.函数名称首字母是大写

为什么这样的构造函数能返回一个实例对象呢?那是因为new运算符会进行如下操作

1.在内存中创建一个新对象

2.这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性

2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)

3.执行构造函数中的代码(为新对象添加属性和方法)

4.如果构造函数返回非空对象,则返回该对象;否则,返回刚才创建的新对象(this指向的对象)

所以最终我们会得到Person的实例。

1.3.2 构造函数当成函数使用

若将构造函数当成函数使用:

// 在全局作用域下使用,this指向window对象,所以属性都添加到window对象上了
Person("window", 100, 6)
window.sayName();    //"window"

//使用call将构造函数作用域绑定到对象o上,所以this指向了o对象
var o = new Object();
Person.call(o, "o", 351, 7)
o.sayName()      //"o"

1.3.3 构造函数创建的实例的类型检测

说到类型检测,其实还是要从原型上来说这个问题,后文再说明。上面通过Person类创建的实例,都可以使用instanceof来检测类型:

console.log(person4 instanceof Object);   //true
console.log(person4 instanceof Person);   //true

1.3.4 构造函数的问题

每个方法都要在新创建的实例上重新创建一遍,即使是功能一模一样的方法。例如上面几个例子中的sayName()。

即构造函数在每次创建一个对象时,下面的语句都会创建一个新的sayName函数,每个实例上的sayName都是独立的函数(不同的内存地址),只是指向这些不同内存地址的变量的名字相同而已

    this.sayName = function(){
        console.log(this.name)
    }

为了解决这个问题,可以将sayName函数提取出来放在全局中:

function Person(name, age, id){
    this.name = name;
    this.age = age;
    this.id = id;
    this.sayName = sayName
}

var sayName = function(){
    console.log(this.name)
}

但是这样会造成全局污染,Person这个引用类型的封装性被破坏

接下来的原型模式就能解决这个问题。

2 原型模式

2.1 原型模式创建对象的方式

原型模式创建对象方式如下:

function Person(){}

Person.prototype.name = "Ma Dongmei";
Person.prototype.age = 43;
Person.prototype.sex = "female"
Person.prototype.sayName = function(){
  console.log(this.name)
}

var person1 = new Person();
person1.sayName();                  //"Ma Dongmei"

// function Animal(name, age, sex){
//   Animal.prototype.name = name;
//   Animal.prototype.age = age;
//   Animal.prototype.sex = sex;
//   Animal.prototype.sayName = function(){
//     console.log(this.name);
//   }
// }

// var dog = new Animal("dog", 1, "male");
// console.log(dog)
// var cat = new Animal("cat", 2, "female")
// console.log(dog)
 

缺点:

1.省略了为构造函数传递初始化参数这一环节,结果所有的实例在默认情况下都取得相同属性值,(注意:不能采用上面代码中被注释掉的构造函数,因为每次使用构造函数创建对象的时候,原型对象都会被修改,所以以前创建的实例会与新实例的属性一致。在上面的例子中,第一次打印的时候,dog还是dog,第二次dog就变成cat了)

2.由于属性都是实例共享的,所以对于引用类型属性来说,在某个实例上修改它,有可能会反应到其他实例对象上。(包含基本值的属性没关系,因为修改这个属性,相当于在实例对象上添加了一个同名属性,它会覆盖掉共享的属性)

function Person(){}

Person.prototype.name = "Ma Dongmei";
Person.prototype.age = 43;
Person.prototype.sex = "female";
Person.prototype.arr = [1,2,3,4]
Person.prototype.sayName = function(){
  console.log(this.name)
}

var person1 = new Person();
var person2 = new Person();
person1.arr.push(5)                //[1,2,3,4,5]

2.2 isPrototypeOf和Object.getPrototypeOf

当调用构造函数创建新的实例对象时,该实例对象内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版管这个指针叫[[Prototype]]。在部分实现中,这个属性完全不可见,但是在FireFox、Chrome、Safari等实现中,都将这个属性设置为__proto__。

isPrototypeOf:

继承自Object,所有对象都可以使用。可以判断某个对象是否是另外一个对象的原型。

console.log(Person.prototype.isPrototypeOf(person1));    //true

Object.getPrototypeOf:

ES5新增的方法,获取一个对象的原型。

console.log(Object.getPrototypeOf(person1) === Person.prototype);    //true
console.log(Object.getPrototypeOf(person2).name)     //Ma Dongmei

2.3 属性查找机制

JavaScript属性查找过程:

1.在实例对象上查找是否有某个属性,有,则使用该属性,没有:

2.在该实例对象的原型上查找是否有这个属性,有,则使用该属性,没有:

3.在该实例对象的原型的原型上查找是否有这个属性,有,则使用该属性,没有:

4.任然顺着原型链继续查找,知道最后__proto__指向null。

5.若指向null后任然没有找到,则返回undefined。

因此:

虽然实例对象可以访问原型上的属性,但是不能通过实例对象重写原型中的值,因为在实例上添加了一个属性,该属性和原型中的属性同名,那么该属性将会屏蔽掉原型中的那个属性。

可以使用delete操作符完全删除实例属性(注意:该属性描述符中的configurable属性应该为true,否则删除无效,在严格模式下还会报错)

2.4 属性检测方法:hasOwnProperty、in运算符

hasOwnProperty():

继承自Object,所有的对象都可使用。检测实例对象上是否存在某个属性。(无论属性修改符enumerable是false还是true,都可以正常检测)

var o = {
  a:1,
  b:2,
  c:3
}

Object.defineProperty(o,"d",{
  value:4
})

console.log(Object.getOwnPropertyDescriptor(o,"d"))   //除了value,其他的全是false
console.log(o.hasOwnProperty("d"))        //true

in:

只要对象能通过原型链找到该属性(无论属性是否可枚举),就返回true。

《JS高三》里封装了一个函数。

function hasPrototypeProperty(obj, name){
  return !obj.hasOwnProperty(name) && (name in obj)
}

该函数仅能判断属性是否在实例对象的原型链上,而不能断定它在实例对象的原型上。

2.5 属性的遍历方法

for...in

遍历对象自身和继承而来的可枚举属性的属性名。

var o = {
  a:1,
  b:2,
  c:3
}

Object.defineProperty(o,"d",{
  value:4
})

for(let key in o){
  console.log(key)                 
}
//a  b  c 

Object.keys(obj):

返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)

Object.getOwnPropertyNames(obj):

返回一个数组,包含对象自身的所有属性,包括不可枚举的属性,不含Symbol属性)

Object.getOwnPropertySymbols(obj):

返回一个数组,包含对象自身所有的Symbol属性。

Reflect.ownKeys(obj):

a返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举。

3 终极方法:组合构造函数模式和原型模式

为了解决上述原型模式和构造函数模式的种种缺点,可以将他们组合使用。

构造函数内用于定义实例属性,原型中定义方法和共享的属性。

function Person(name, age, sex, ...friends){
  this.name = name;
  this.age = age;
  this.sex = sex;
  this.friends = friends;
}

Person.prototype.sayHello = function(){
  console.log(this.name)
}
  Person.prototype.enemy = ["佩恩","角度","迪达拉"]
var person1 = new Person("那撸多",18,"male","萨斯给","撒库拉","hi那他")
console.log(person1);
person1.sayHello();

优点:

1.每一个实例都有一份实例属性副本,同时又共享方法的引用

2.支持向构造函数传递参数进行初始化。

原文地址:https://www.cnblogs.com/lilisblog/p/13223664.html