前言
-
最近学习
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__);
2. 基本类型到底是对象吗?
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();