前言
-
最近学习
JavaScript中的原型这个概念,想要彻底了解下JavaScript是基于原型的编程语言这个说法。 -
网上关于原型对象和原型链的文章也是一搜一大把,于是跟着大部队的思路,从构造函数这个概念入手,认认真真反复琢磨构造函数的
prototype和对象的__proto__的关系,欢天喜地地认为:“我终于知道原型对象和原型链了,啥也不是事,哈哈哈!” -
但是,我的心还是剪不断啊理还乱,感觉这个构造函数也好,这个原型对象也罢,好像和
Java(本人是先学习的Java,后接触的JavaScript中的构造函数)中的类的使用方式区别也不大呀,不就是定义一个构造函数(比如创建一个对象Student),然后在Student的原型对象上Student.prototype上定义属性或方法供new出来的实例(对象)使用(他们都可以调用父辈的方法),有啥不一样的?
function Student(name, sex, age, grade, hobby) {
this.name = name;
this.sex = sex;
this.age = age;
this.grade = grade;
this.hobby = hobby;
}
Student.prototype.type = "person";
Student.prototype.play = function() {
return this.name + " play games";
}
Student.prototype.read = function() {
return this.name + " read books";
}
let Lucy = new Student("lucy", "女", "15", ["reading","travel","singing"]);
Lucy.play();
-
看起来,除了构造函数是用
function修饰的,调用方式不也一样一样的吗?我们在调用Student的过程中,一般也不会去再定义Lucy其他的属性,所以他们的区别是啥? -
原来我是被构造函数和
new蒙蔽了双眼 其实new、Function、Object、函数的prototype这一些都是JavaScript模拟Java的语法,抛开这些,其实原型系统很简单(下面2条看不懂的话,可以先看完全篇再回过头来体会):A) 每个对象都有一个私有字段(隐藏属性)
<prototype>:这个就是对象的原型B)读取一个属性,若对象本身没有,则会继续访问对象的原型,直到原型为空或找到为止。
-
这里先提下个人 现在(目前为止) 对“基于原型对象” 和 “基于类” 的通俗理解:
基于原型对象:
1. 有一个对象 猫Cat
2. 根据猫Cat这个原型 创造一个新对象大猫“老虎”
3. 为大猫添加新的属性
4. 根据猫Cat, 创造其他猫科动物
5. 为其他动物添加他们特有的属性
6. 猫、大猫、其他猫科动物,他们都是一个对象
7. 所以原型对象是创建新对象的参照对象
基于类:
1. 有一个类 猫科动物 CatAnimal
2. 创建一个猫实例 cat
3. cat调用类的方法,不可动态添加新的属性和方法
4. 再次创建一个实例虎tiger
5. tiger和cat的所拥有的属性和方法是固有的,
6. 类CatAnimal描述的是他们的共有特征
7. 所以类是一个概念,是一类事务共有特征的抽象
JavaScript对象
在正式开始学习JavaScript中的原型对象之前,先来看看JavaScript中的对象及其特征。
JavaScript中的七种数据类型
number、string、boolean、symbol、undefined、null、object(引用类型)。我们所见到的数组以及函数等,并不是独立的一种数据类型,而是包含在object里的。
JavaScript对象的特征
- 对象具有唯一标识性
let o1 = {name:"Lucy"}
let o2 = {name:"Lucy"}
o1 == 02 //false
- 对象具有状态
- 对象具有行为
对于第2点和第3点,不同的语言有不同的术语来表达
// Java: 状态=>属性,行为=> 方法
// JS中: 统一称为属性,下面的play也称为属性
let obj = {
name : "Lucy",
age: 20,
play: function() {
return "Lucy play basketball";
}
}
JavaScript独有的特色:可以动态改变属性(即动态修改状态和行为)
JS对象中的两种类型的属性
- 数据属性
数据属性包含四个特性,分别是:
configurable:能否被删除或被改变
enumerable:能否被遍历
writable:能否被赋值
value:属性值
//定义数据类型一般方案
let obj = {a : 1}
//定义数据类型方案2
Object.defineProperty(obj,'b',{
configurable:true,
enumerable:true,
writable:true,
value:1
})
- 访问器属性
访问器属性也包含四个特性,分别是:
configurable:能否被删除或被改变
enumerable:能否被遍历
getter:取值
setter:设置属性值
let obj = {
_age: 1, //下划线表示内部属性,
//定义访问器属性的方案1
get age(){
return this._age
},
set age(nv){
this._age = nv;
}
}
console.log(obj.age) //1
obj.age = 2;
console.log(obj.age) //2
//定义访问器属性的方案2
Object.defineProperty(obj,'sex',{
configurable:true,
enumerable:true,
get:function(){
return this._sex;
},
set:function(newValue){
this._sex = newValue;
}
})
- Object几个相关的方法掌握下
Object.defineProperty():通过描述对象,定义某个属性Object.defineProperties():通过描述对象,定义多个属性Object.getOwnPropertyDescriptor():获取某个属性的描述对象Object.getOwnPropertyNames():遍历对象的属性
构造函数中的原型
虽然前言中说到“我被构造函数蒙蔽了双眼”,但是这块知识点还是很重要的,在es6之前,JS中的面向对象编程,一般都是通过构造函数来实现的吧。
prototype属性
构造函数有一个隐藏属性 prototype
对象有一个隐藏属性<prototype>,可能说成__proto__ 大家更为熟悉(这个属性才是 传说中的 原型对象)。
构造函数 constructor
-
构造函数的首字母必须大写,用来区分于普通函数
-
内部使用的this对象,来指向即将要生成的实例对象
-
使用new来生成实例对象
-
作用:实现面向对象编程
-
案例:
/**
name/age/nation/sayHello 都是明确定义的属性
还有隐藏属性prototype和<prototype>(__proto__)
*/
function Person(name, age) {
this.name = name;
this.age = age;
this.nation = "China";
this.sayHello = function() {
console.log("hello !");
}
}
Person.prototype.play = function(){
return "play games";
}
let lucy = new Person('Lucy',20);
- new 命令的原理? 阮一峰的面向对象编程
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的prototype属性。
- 将这个空对象赋值给函数内部的this关键字。
- 开始执行构造函数内部的代码。
对象与函数
- 对象类型
lucy:具体的一个对象
Person: 构造函数,也是一个对象
Person中定义的属性也是对象(基本类型到底是对象吗?)
(当然隐藏属性prototype也是对象)
js中没有类和实例的说法
只要是对象,就有<prototype>(__proto__)这个属性
//打印下上面lucy和Person的__proto__
console.log("lucy的__proto__")
console.log(lucy.__proto__);
console.log("Person的__proto__")
console.log(Person.__proto__);
let obj = {
name: "Lucy",
age: 20
}
console.log(obj.name.__proto__)
console.log(obj.name.__proto__.__proto__)
console.log(obj.age.__proto__)
console.log(obj.age.__proto__.__proto__)
对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper)。
所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false
上面代码中,基于原始类型的值,生成了三个对应的包装对象。可以看到,v1、v2、v3都是对象,且与对应的简单类型值不相等。
包装对象的设计目的,首先是使得“对象”这种类型可以覆盖 JavaScript 所有的值,整门语言有一个通用的数据模型,其次是使得原始类型的值也有办法调用自己的方法。
- 函数:被function修饰,数据类型为Function
/**
上面提到的原型对象prototyp这个属性,只有函数对象才有;
除了构造函数,普通函数也有
我们主要了解构造函数有这个即可!!!
*/
function test(){
return "test";
}
console.log(test.prototype)
//而test.prototype也是一个对象,所以它有__proto__属性
//打印下上面构造函数Person的prototype属性
console.log(Person.prototype)
原型对象概述
这一节的知识点拷贝自: 阮一峰的原型对象
构造函数的缺点
JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。
function Cat (name, color) {
this.name = name;
this.color = color;
}
var cat1 = new Cat('大毛', '白色');
cat1.name // '大毛'
cat1.color // '白色'
上面代码中,Cat函数是一个构造函数,函数内部定义了name属性和color属性,所有实例对象(上例是cat1)都会生成这两个属性,即这两个属性会定义在实例对象上面。
通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
function Cat(name, color) {
this.name = name;
this.color = color;
this.meow = function () {
console.log('喵喵');
};
}
var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');
cat1.meow === cat2.meow
// false
上面代码中,cat1和cat2是同一个构造函数的两个实例,它们都具有meow方法。由于meow方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow方法。这既没有必要,又浪费系统资源,因为所有meow方法都是同样的行为,完全应该共享。
这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。
prototype 属性的作用
JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。
下面,先看怎么为对象指定原型。
JavaScript规定,每个函数都有一个prototype属性,指向一个对象。
function f() {}
typeof f.prototype // "object"
上面代码中,函数f默认具有prototype属性,指向一个对象。
对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
function Animal(name) {
this.name = name;
}
Animal.prototype.color = 'white';
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
cat1.color // 'white'
cat2.color // 'white'
上面代码中,构造函数Animal的prototype属性,就是实例对象cat1和cat2的原型对象。原型对象上添加一个color属性,结果,实例对象都共享了该属性。
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
Animal.prototype.color = 'yellow';
cat1.color // "yellow"
cat2.color // "yellow"
上面代码中,原型对象的color属性的值变为yellow,两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。
如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
cat1.color = 'black';
cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';
上面代码中,实例对象cat1的color属性改为black,就使得它不再去原型对象读取color属性,后者的值依然为yellow。
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。
Animal.prototype.walk = function () {
console.log(this.name + ' is walking');
};
上面代码中,Animal.prototype对象上面定义了一个walk方法,这个方法将可以在所有Animal实例对象上面调用。
原型链
对象与函数
对象链
let foo = new Foo();
/**
foo是一个对象,它有__proto__属性
且该属性指向构造函数Foo的原型属性prototype
*/
foo.__proto__ == Foo.prototype
/**
原型对象Foo.prototype也是一个对象,
它的__proto属性指向Object构造函数的prototype
*/
Foo.prototype.__proto__ == Object.prototype
/**
那原型对象Object.prototype自身也是一个对象呀
那Object.prototype的__proto__指向谁呢?
*/
Object.prototype.__proto__ == null
//所以从对象这条线来看,他们的关系是
foo.__proto__.__proto__.__proto__ == null
函数链
/**
构造函数Foo也是对象,
它的__proto__又指向谁呢?
*/
Foo.__proto__ = Function.prototype
//所以,没有继承等关系的构造函数的
//__proto__都指向Function.prototype
/**
Function是个特殊的构造函数吧!
和之前的Foo.prototype.__proto__结果一致
*/
Function.prototype.__proto__ == Object.prototype
/**
哈哈哈,又到这里了。
Object当然也是一个特殊的构造函数了
*/
Object.prototype__proto__ == null
/**
那,那那...
Function和Object的__proto__又都指向哪里呢?
说了,他们俩不过是特殊的构造函数,
其实和Foo也没多大差别是吧
所以...
*/
Function.__proto__ == Function.prototype
Object.__proto__ == Function.prototype
//而Function又只是一个稍微特殊的对象
Function.prototype.__proto__ == Object.prototype
//所以从函数对象这条线来看,他们的关系是
Foo.__proto__.__proto__.__proto__ == null
经典图
(网上流传的经典图,本人是上面的关系理清了来看这张图才看的懂)
总结
__proto__:在JavaScript中,每个对象都拥有一个原型对象,而指向该原型对象的内部指针则是__proto__,通过它可以从中继承原型对象的属性,原型是JavaScript中的基因链接,有了这个,才能知道这个对象的祖祖辈辈。从对象中的__proto__可以访问到他所继承的原型对象。prototype:函数对象除了拥有__proto__属性之外,还拥有prototype属性。通过该函数构造的新的实例对象,其原型指针__proto__会指向该函数的prototype属性。而函数的prototype属性,本身是一个由Object构造的实例对象。- 原型链:‘对象实例’通过指针
__proto__指向原型对象prototype形成的那个链条关系
对象的继承
直接看这里吧JavaScript常用八种继承方案
ES6中的类与对象的创建
Class类
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
ES6直接访问操纵原型对象
Object.setPrototypeOf():设置指定对象的原型
Object.getPrototypeOf():返回指定对象的原型
Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
const person = {
isHuman: false,
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};
const me = Object.create(person);
me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten
me.printIntroduction();