原型对象
原型对象
每一个函数在创建时都会被赋予一个prototype属性,它指向函数的原型对象,这个对象可以包含 所有实例共享的属性和函数,因此在使用prototype属性后,就可以将实例共享的属性和函数抽离出构造函数,将它添加在prototype属性中。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName === person2.sayName);
实例共享的sayName()
函数就被添加在了Person.prototype
属性上,通过测试我们会发现不同实例中的**sayName
属性是相等**的。
使用prototype属性就很好的解决了单纯通过构造函数创建实例会导致函数在不同实例中重复创建的问题。
原型对象,构造函数和实例
构造函数的prototype
属性会指向它的原型对象,而通过构造函数可以生成具体的实例
原型对象:
-
原型对象,构造函数和实例的关系?
每个构造函数都有一个原型对象,原型对象上包含着一个指向构造函数的指针,而实例都包含着一个指向原型对象的内部指针。通俗来说,实例可以通过内部指针访问到原型对象,原型对象可以通过constructor找到构造函数
-
使用原型对象创建了对象的实例后,实例的属性读取顺序是什么样的?
实例对象必须在原型对象后创建
-
假如重写了原型对象,会带来什么样的问题?
重写原型对象, 其
constructor
属性也随之消失. 通常我们会将constructor
’属性重制, 而对象与对象之间的继承关系实际上就是.__proto__
和.prototype
间的关系.
原型对象,构造函数和实例之间的关系
- 每一个函数在创建时都会被赋予一个prototype属性,它指向函数的原型对象,在默认情况下,所 有的原型对象都会增加一个constructor属性,指向prototype属性所在的函数,即构造函数。
- 通过new操作符调用构造函数创建一个实例时,实例具有一个
__proto__
属性,指向构造函 数的原型对象,因此__proto__
属性可以看做是一个连接实例与构造函数的原型对象的桥梁。
function Person() {}
Person.prototype.name = 'cao teacher';
Person.prototype.age = 29;
Person.prototype.job = 'code teacher';
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName === person2.sayName);//true
以上为例,看原型对象和实例之间的关系
- 构造函数
Person
有个prototype
属性,指向的是Person
的原型对象,在原型对象中有constructor
属 性和另外4个原型对象上的属性,其中construct
属性指向构造函数本身。 - 通过
new
操作符创建的两个实例person1
和person2
,都具有一个__proto__
属性指向的是Person
原型对象。
实例的属性读取顺序
当我们通过对象的实例读取某个属性时,是有一个搜索过程的。它会先在实例本身去找指定的属 性,如果找到了,则直接返回该属性的值,如果没找到,则会继续沿着原型对象寻找,如果在原型对象 中找到了该属性,则返回该属性的值。
按照前面的实例,假如我们需要输出person1.name
属性,会现在person1
实例本身中寻找name
属 性,而person1
本身并没有该属性,因此会继续沿着原型对象寻找,在prototype
原型对象上寻找到了name
属性值"cao teacher",
因此最终会输出"cao teacher"
。
function Person() {
this.name = 'caoteacher';
}
Person.prototype.name = 'cao teacher2';
Person.prototype.age = 29;
Person.prototype.job = 'code teacher';
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
console.log(person1.name);//caoteacher
我们在Person()
构造函数中新增了一个name
属性,它是一个实例属性,当我们需要输出 person1.name
属性时,会先在person1
实例本身中寻找name
属性,能够找到该属性值为'cao teacher'
, 因此输出'caoteacher'
。
同样,假如Person()
构造函数同时具有相同名称的实例属性和原型对象上的属性,在生成实例后, 删除了实例的实例属性,那么会输出原型对象上的属性的值。
function Person() {
//这里的name是实例属性
this.name = 'caoteacher';
}
//这里的name是原型对象上的属性
Person.prototype.name = 'cao teacher2';
var person1 = new Person();
//删除实例的属性
delete person1.name;
console.log(person1.name);//cao teacher2
重写原型对象
每次为原型对象添加一个属性或者函数时,都需要手动写上Person.prototype
, 这是一种冗余写法,我们可以将所有需要绑定在原型对象上的属性写成一个对象字面量的形式,并赋值 给prototype
。
function Person() {}
Person.prototype = {
constructor: Person,
//重要
name: 'cao teacher2',
age: 29,
job: 'code teacher',
sayName: function () {
console.log(this.name);
}
};
当我们创建person对象的实例时仍然可以正常的访问各个原型对象上的属性。
将一个对象字面量赋给prototype属性的方式实际是重写了原型对象,等同于切断了构造函数和最初原型之间的关系,因此有一点需要注意的是,如果仍然想使用constructor属性做后续处理,则应该在对象字面量中增加一个constructor属性,指向构造函数本身,否则原型的constructor属性会指向 Object类型的构造函数,从而导致constructor属性与构造函数的脱离。如下。
function Person() {}
Person.prototype = {
name: 'cao teacher2',
sayName: function () {
console.log(this.name);
}
}
console.log(Person.prototype.constructor === Object);//true
console.log(Person.prototype.constructor === Person);//false
原型链
原型链
对象的每个实例都具有一个 __proto__
属性,指向的是构造函数的原型对象,而原型对象同样存在 一个__proto__
属性指向上一级构造函数的原型对象,就这样层层往上,直到最上层某个原型对象为 null
。 在JavaScript中几乎所有的对象都具有__proto__
属性,由__proto__
属性链接而成的链路构成了JavaScript
的原型链,原型链的顶端Object.prototype
,他的__proto__
属性为null
。
原型链的特点
- 由于原型链的存在,属性查找的过程不再是只查找自身的原型对象,而是会沿着整个原型链一直向上,直到追溯到
Object.prototype
,如果Object.prototype
上也找不到该属性,则返 回undefined
,如果期间在实例本身或者某个原型对象上找到了该属性,则会直接返回结果,因此 会存在属性覆盖的问题, 由于特点1的存在,我们在生成在定义对象的实例时,也可以调用到某些未在在定义构造函数上的 函数,例如toString()函数。
function Person(){}
var p = new Person();
p.toString();
//Object Object 实际调用的是Object.prototype.toString()函数
- 由于属性查找会经历整个原型链,因此查找的链路越长,对性能的影响越大。
属性区分
怎么区分属性是实例自身还是从原型链中继承的呢?
Object()构造函数的原型对象中提供了一个hasOwnProperty()
函数,用于判断属性是否为自身拥有的。
function Person(name) {
//实例属性name
this.name = name;
}
//原型对象上的属性age
Person.prototype.age = 12;
var person = new Person('cao teacher');
console.log(person.hasOwnProperty('name'));//true
console.log(person.hasOwnProperty('age'));//false
内置构造函数
javaScript
中有一些特定的内置构造函数,如String()函数,Number()构造函数,Array()构造函数, Object()构造函数等。 它们本身的__proto__
属性都统一指向Function.prototype
console.log(String.__proto__ === Function.prototype);//true
console.log(Number.__proto__ === Function.prototype);//true
console.log(Array.__proto__ === Function.prototype);//true
console.log(Object.__proto__ === Function.prototype);//true
console.log(Date.__proto__ === Function.prototype);//true
console.log(Function.__proto__ === Function.prototype);//true
__proto__
属性
在JavaScript的原型链体系中,最重要的莫过于__proto__
属性,只有通过它才能将原型链串联起 来。
var str = new String('cao teacher');
console.log(str);
str
的值包含10个字符和1个length属性。 但是我们在调用str.substring(1,3)
时,却不会报错,这是为什么呢? 因为__proto__
属性可以沿着原型链找到String.prototype
中的函数,而substring()
函数就在其中。
Function.prototype.a='a';
Object.prototype.b='b';
function Person(){}
var p = new Person;
console.log('p.a',p.a);//p.a undefined
console.log('p.b',p.b)//p.b b
//实例p的原型链
P.__proto__ = Person.prototype;
//Person原型对象的原型
Person.prototype.__proto__ = Object.prototype;
继承
继承作为面向对象语言的三大特性之一,三大特性是(继承,封装,多态),可以在不影响父类对象 实现的情况下,使得子类对象具有父类对象的特性,同时还能在不影响父类对象行为的情况下扩展子类 对象独有的特性,为编码带来了极大的便利。
虽然JavaScript并不是一门面向对象的语言,不直接具备继承的特性,但是我们可以通过某些方式间 接实现继承,从而能利用继承的优势,增强代码复用性与扩展性。
//定义一个父类Animal
function Animal(name){
//属性
this.type = 'Animal';
this.name = name || '动物';
//实例函数
this.sleep = function(){
console.log(this.name+'正在睡觉');
}
}
//原型函数
Animal.prototype.eat = function(food){
console.log(this.name + '正在吃:'+food);
}
原型链继承
主要思想是:重写子类的prototype属性,将其指向父类的实例。
定义一个子类Cat,用于继承父类Animal
//子类Cat
function Cat(name) {
this.name = name;
}
//原型继承
Cat.prototype = new Animal();
//很关键的一句,将Cat的构造函数指向自身
Cat.prototype.constructor = Cat;
var cat = new Cat('加菲猫');
console.log(cat.type);
console.log(cat.name);
console.log(cat.sleep());
console.log(cat.eat('猫粮'));
在子类Cat中,我们没有增加type属性,因此会直接继承父类Animal的type属性,输出字符 串’Animal’。
在子类Cat中,我们增加了name属性,在声称子类Cat的实例时,name属性值会覆盖父类Animal 的name属性值,因此输出字符串"加菲猫",而并不会输出父类Animal的name属性"动物"。
因为Cat的prototype属性指向了Animal类型的实例,因此在生成实例cat时,会继承实例函数 和原型函数,在调用sleep()函数和eat()函数时,this指向了实例cat,从而输出"加菲猫正在睡觉!“和"加 菲猫正在吃猫粮”。
//很关键的一句,将Cat的构造函数指向自身Cat.prototype.constructor = Cat;
因为如果不将Cat原型对象的constructor属性指向自身的构造函数的话,那将会指向父类 Animal的构造函数。
原型链继承的优缺点
原型链继承的优点
-
简单,容易实现
只需要设置子类的prototype属性为父类的实例即可,实现起来简单。
-
继承关系纯粹
生成的实例即是子类的实例,也是父类的实例。
-
可以通过子类直接访问父类原型链属性和函数
通过原型链继承的子类,可以直接访问到父类原型链上新增的函数和属性。 继续沿用前面的代码,我们通过在父类的原型链上添加属性和函数进行测试
//父类原型链上增加属性
Animal.prototype.bodyType = 'small';
//父类原型链上增加函数
Animal.prototype.run = function () {
return this.name + '正在奔跑';
};
//结果验证
console.log(cat.bodyType);
console.log(cat.run());
原型链继承的缺点
-
子类的所有实例将共享父类的属性
使用原型链继承时,直接改写了子类Cat的prototype属性,将其指向一个Animal的实例,那 么所有生成Cat对象的实例都将会共享Animal实例的属性。
//生成一个Animal的实例animal var animal = new Animal(); //通过改变Cat的原型链,所有的Cat实例将共享animal中的属性 Cat.prototype = animal;
-
在创建子类实现时,无法向父类的构造函数传递参数
在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置 与父类的关联,从而导致无法向父类的构造函数传递参数。
-
无法实现多继承
由于子类Cat的prototype属性只能设置为一个值,如果同时设置为多个值的话,后面的值会覆盖前面的值,导致Cat只能继承一个父类,而无法实现多继承。
-
为子类增加原型对象上的属性和函数时,必须放在new Animal()函数之后,因为如果在这 个语句之前设置了prototype属性,后面执行的语句会直接重写prototype属性,导致之前的设置全部失效。
Cat.prototype = new Animal();
构造继承
构造继承
构造继承的主要思想是在子类的构造函数中通过call()函数改变this的指向,调用父类的构造函数, 从而能将父类的实例的属性和函数绑定到子类的this上。
//定义一个父类Animal
function Animal(age) {
//属性
this.name = 'Animal';
this.age = age;
//实例函数
this.sleep = function () {
console.log(this.name + '正在睡觉');
}
}
//父类原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃' + food;
};
//子类
function Cat(name) {
//核心通过call()函数实现Animal的实例的属性和函数的继承
Animal.call(this);
this.name = name || 'Tom';
}
//生成子类的实例
var cat = new Cat('tony');
//可以正常调用父类实例函数
cat.sleep();
//不能调用父类原型函数
cat.eat();
子类可以正常调用父类的实例函数,而无法调用父类原型对象上的函数,这是 因为子类并没有通过某种方式来调用父类原型对象上的函数。
构造继承的优缺点
构造继承的优点
-
可解决子类实例共享父类属性的问题。
call()函数实际是改变了父类Animal构造函数中this的指向,调用后this指向了子类Cat,相当于父类 的type,age和sleep等属性和函数直接绑定到了子类的this中,成了子类实例的属性和函数,因此 生成的子类实例中是各自拥有自己的type,age和sleep属性和函数,不会互相影响。
-
创建子类的实例时,可以向父类传递参数
//定义一个父类Animal function Animal(age) { //属性 this.name = 'Animal'; this.age = age; //实例函数 this.sleep = function () { console.log(this.name + '正在睡觉'); } } //父类原型函数 Animal.prototype.eat = function (food) { return this.name + '正在吃' + food; }; //子类 function Cat(name, parentAge) { //在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承 Animal.call(this, parentAge); this.name = name || 'Tom'; } //生成子类的实例 var cat = new Cat('tony', 11); //可以正常调用父类实例函数 console.log(cat.age);//11
-
可以实现多继承
通过多次调用call()函数来继承多个父对象,每调用一次call()函数就会将 父类的实例的属性和函数绑定到子类的this中。
构造继承的缺点
- 实例只是子类的实例,并不是父类的实例。
原因:因为我们并没有通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系,这样就 失去了继承的意义。
-
只能继承父类实例的属性和函数,并不能继承原型对象上的属性和函数。
-
无法复用父类的实例函数
由于父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例函数的引用,这会造成不必要的内存消耗,影响性能。
复制继承
主要思想:
- 首先生成父类的实例
- 然后通过for…in遍历父类实例的属性和函数
- 并将其依次设置为子类实例的属性和函数或者原型对象上的属性和函数。
//定义一个父类Animal
function Animal(parentAge) {
//实例属性
this.name = 'Animal';
this.age = parentAge;
//实例函数
this.sleep = function () {
console.log(this.name + '正在睡觉');
}
}
//原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃' + food;
};
//子类
function Cat(name, age) {
var animal = new Animal(age);
//父类的属性和函数添加到子类中
for (var key in animal) {
//实例属性和函数
if (animal.hasOwnProperty(key)) {
this[key] = animal[key];
} else {
//原型对象上的属性和函数
Cat.prototype[key] = animal[key];
}
}
//子类自身的属性
this.name = name;
}
//子类自身原型函数
Cat.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
}
var cat = new Cat('tony', 12);
console.log(cat.age);
cat.sleep();
console.log(cat.eat('猫粮'));
-
animal.hasOwnProperty(key)
返回"true"
,则表示是实例的属性和函数,则直接绑定到子类的this上,成为子类实例的属性和函数 -
animal.hasOwnProperty(key)
返回“false”
,则表示是原型对象上的属性和函数,则将其添加至子类的 prototype属性上,成为子类的原型对象上的属性和函数。 -
生成的子类实例 cat 可以访问到继承的age属性,同时还能够调用继承的sleep()函数与自身原型对 象上的eat() 函数。
复制继承的优缺点
复制继承的优点
-
支持多继承
只需要在子类的构造函数中生成多个父类的实例,然后通过相同的for…in处理即可
-
能同时继承实例的属性和函数与原型对象上的属性和函数
对所有的属性进行for…in 处理时,会通过
hasOwnProperty()
函数判断其是实例的属性和函数 还是原型对象上的属性和函数,并根据结果进行不同的设置,从而既能继承实例的属性和函数又能 继承原型对象上的属性和函数 -
可以向父类构造函数中传递值
在生成子类的实例时,可以在构造函数中传递父类的属性值,然后在子类构造函数中,直接将值传 递给父类的构造函数。
复制继承的缺点
-
父类的所有属性都需要复制,消耗内存。
对于父类的所有属性都需要复制一遍,这回造成内存的重复利用,降低性能。
-
实例只是子类的实例 ,并不是父类的实例
实际上我们只是通过遍历父类的属性和函数并将其复制到子类上,并没有通过原型对象串联起父类和子类,因此子类的实例不是父类的实例。
组合继承
组合继承
主要思想:
- 组合了构造继承和原型继承两种方法
- 一方面在子类的构造函数中通过call() 函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中,
- 另一方面,通过改变子类 的prototype属性,继承父类的原型对象上的属性和函数。
//定义一个父类Animal
function Animal(parentAge) {
//实例属性
this.name = 'Animal';
this.age = parentAge;
//实例函数
this.sleep = function () {
console.log(this.name + '正在睡觉');
};
this.feature = ['fat', 'thin', 'tall'];
}
//原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
//子类
function Cat(name) {
//通过构造函数继承实例的属性和函数
Animal.call(this);
this.name = name;
}
//通过原型继承原型对象上的属性和函数
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat('tony');
console.log(cat.name);
cat.sleep();
console.log(cat.eat("猫粮"));
组合继承的优缺点
组合继承的优点
-
既能继承父类实例的属性和函数,又能继承原型对象的属性和函数
通过
Animal.call(this)
可以将父类实例的属性和函数绑定到Cat构造函数的this中,另一方 面,通过Cat.prototype = new Animal
可以将父类的原型对象上的属性和函数绑定到Cat原型对象上。 -
既是子类的实例,又是父类的实例
-
不存在引用属性共享的问题
因为在子类的构造函数中已经将父类的实例属性指向了子类的this,所以即是后面将父类的实例属性绑定到子类的prototype属性中,也会因为构造函数作用域优先级比原型链优先级高,所以不会出现引用属性共享的问题
-
可以向父类的构造函数中传递参数
通过call()函数可以向父类的构造函数中传递参数。
组合继承的缺点
组合继承的缺点为父类的实例属性会绑定两次
(在子类的构造函数中,通过call()函数调用了一次父类的构造函数,在改写子类的prototype属性, 生成父类的实例时调用了一次父类的构造函数。)
寄生组合继承
寄生组合继承
在进行子类的prototype属性的设置时,可以去掉父类实例的属性和函数。
//定义一个父类Animal
function Animal(parentAge) {
//实例属性
this.name = 'Animal';
this.age = parentAge;
//实例函数
this.sleep = function () {
console.log(this.name + '正在睡觉');
};
this.feature = ['fat', 'thin', 'tall'];
}
//原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
//子类
function Cat(name) {
//继承父类的实例属性和函数
Animal.call(this);
this.name = name;
}
//立即执行函数
(function () {
//设置任意函数Super()
var Super = function () {};
//关键语句 Super()函数的原型指向父类Animal的原型,去掉父类的实例属性
Super.prototype = Animal.prototype;
Cat.prototype = new Super();
Cat.prototype.constructor = Cat;
})();
var cat = new Cat('tony');
console.log(cat.name);
console.log(cat.sleep());
console.log(cat.eat("猫粮"));
Object类型及其实例和静态函数
JavaScript中的new运算符
由于引用数据 类型的实例都需要通过new操作符来生成,因此我们需要了解new操作符的相关知识。 new操作符 在执行过程中会改变this的指向,所以了解new操作符之前,我们先了解一下this的用 法。
function Cat(name,age){
console.log(this);
this.name = name;
this.age = age;
}
new Cat('miaomiao',18);
function Cat(name,age){
var Cat = {};
Cat.name = name;
Cat.age = age;
return Cat;
}
console.log(new Cat('miaomiao',18));
this的实际值是Cat的空对象,后两句就相当于给Cat对象添加name和age属性
如果函数没有return值,则默认return this。而上面代码中的this实际是一个 Cat空对象,name和age属性只是被添加到了临时变量Cat中,为了能让输出结果包含name和age属 性,我们将临时变量Cat进行return就行了
使用new操作符做了三件事情
var cat = {};
cat.__proto__ = Cat.prototype;
Cat.call(cat);
HTMLCollection对象与NodeList对象
HTMLCollection对象
主要是对children
属性和childNodes
属性的调用。
<div id="main">
<p class="first">first</p>
<p class="second">second<span>content</span></p>
</div>
<script>
var main = document.getElementById('main');
console.log(main.children);
console.log(main.childNodes);
HTMLCollection对象具有length属性,返回集合的长度,可以通过item()函数和namedItem()函数 来访问特定的元素。
item()函数
HTMLCollection
对象可以调用Item()函数,通过序号来获取特定的某个节点,超过索引则返 回"null"。
<div id="main">
<p class="first">first</p>
<p class="second">second</p>
<p class="three">three</p>
<p class="four">four</p>
</div>
<script>
var main = document.getElementById('main').children;
console.log(main.item(0));//<p class="first">first</p>
console.log(main.item(2));//<p class="three">three</p>
</script>
namedIrem()函数
namedIrem()
函数用于返回一个节点,首先通过id属性去匹配,如果没有匹配到则使用name属性 匹配,如果还没有匹配到则返回"null"。当出现重复id或者name属性时,只返回第一个匹配到的值。
<form id="main">
<input type="text" id="username">
<input type="text" name="username">
<input type="text" name="password">
</form>
<script>
var main = document.getElementById('main').children;
console.log(main.namedItem('username'));
</script>
NodeList对象
NodeList
对象也具有length
属性,返回集合的长度,也同样具有item()
函数,和HTMLCollection
对 象中的效果一样。
HTMLCollection
对象和NodeList
对象并不是历史文档状态的静态快照,而是具有实时性的,对 DOM树新增或者删除一个相关节点,都会立刻反应在HTMLCollection
对象与NodeList
对象中。
HTMLCollection和NodeList的异同
相同点
- 都是类数组对象,有length属性,可以通过call函数获apply函数处理成真正的数组。
- 都有item()函数,通过索引定位元素。
- 都是实时性的。
不同点
HTMLCollection
比NodeList
多了一个namedItem()
函数。HTMLCollection
对象只包含元素的集合,即具有标签名的元素,而NodeList
对象时节点的集合, 即包含元素,也包括节点。
text" name=“username”>
NodeList对象
NodeList
对象也具有length
属性,返回集合的长度,也同样具有item()
函数,和HTMLCollection
对 象中的效果一样。
HTMLCollection
对象和NodeList
对象并不是历史文档状态的静态快照,而是具有实时性的,对 DOM树新增或者删除一个相关节点,都会立刻反应在HTMLCollection
对象与NodeList
对象中。
HTMLCollection和NodeList的异同
相同点
- 都是类数组对象,有length属性,可以通过call函数获apply函数处理成真正的数组。
- 都有item()函数,通过索引定位元素。
- 都是实时性的。
不同点
HTMLCollection
比NodeList
多了一个namedItem()
函数。HTMLCollection
对象只包含元素的集合,即具有标签名的元素,而NodeList
对象时节点的集合, 即包含元素,也包括节点。