JS重难点之一:原型链和继承
1 原型解决的问题
在总结原型之前,先简单回顾一下较为简单常用的创建对象的方式。
1.1 对象字面量和new Object()
对象字面量创建方式:
var person1 = { name: "Li Xiaoming", age: 18, id: 1, sayName: function(){ console.log(this.name) } }
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"
缺点:
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) }
var person1 = new Person("那撸多",18,"male","萨斯给","撒库拉","hi那他") console.log(person1); person1.sayHello();
优点:
1.每一个实例都有一份实例属性副本,同时又共享方法的引用
2.支持向构造函数传递参数进行初始化。