在JavaScript
中,对象是一组无序的相关属性和方法的集合,所有的事物都是对象,例如字符串、数值、数组、函数等。
对象是由属性和方法组成的。
- 属性:事物的特征,在对象中用属性来表示(常用名词)
- 方法:事物的行为,在对象中用方法来表示(常用动词)
0. 创建对象的三种方式
0.1 使用对象字面量创建对象:
就是花括号 { } 里面包含了表达这个具体事物(对象)的属性和方法;{ } 里面采取键值对的形式表示
var star = {
name : 'Jon',
age : 18,
sex : '男',
sayHi : function(){
alert('我好帅');
}
};
0.2 利用 new Object 创建对象
// 创建空对象
var andy = new Obect();
通过内置构造函数Object创建对象,此时andy变量已经保存了创建出来的空对象。
// 通过对象操作属性和方法的方式,来为对象增加属性和方法
andy.name = '张三';
andy.age = 18;
andy.sex = '男';
andy.sayHi = function(){
alert('我好帅~');
}
注意:Object()
:第一个字母大写
0.3 利用构造函数创建对象
构造函数:是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 运算符一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
构造函数的封装格式:
function 构造函数名(形参1,形参2,形参3) {
this.属性名1 = 参数1;
this.属性名2 = 参数2;
this.属性名3 = 参数3;
this.方法名 = 函数体;
}
构造函数的调用格式:
var obj = new 构造函数名(实参1,实参2,实参3)
以上代码中,obj即接收到构造函数创建出来的对象。
注意事项:
- 构造函数约定首字母大写(建议这样操作)。
- 函数内的属性和方法前面需要添加 this ,表示当前对象的属性和方法。
- 构造函数中不需要 return 返回结果。
- 当我们创建对象的时候,必须用 new 来调用构造函数。
使用构造函数创建一个对象的过程:
-
在构造函数代码开始执行之前,创建一个空对象;
-
修改this的指向,把this指向创建出来的空对象;
-
执行函数的代码
-
在函数完成之后,返回创建出来的对象
1. 变量、属性、函数、方法的区别
1.1 属性和变量的区别:
属性是对象的一部分,而变量不是对象的一部分,变量是单独存储数据的容器
变量:单独声明赋值,单独存在
属性:对象里面的变量称为属性,不需要声明,用来描述该对象的特征
1.2 方法和函数的区别:
方法是对象的一部分,函数不是对象的一部分,函数是单独封装操作的容器
函数:单独存在的,通过“函数名()
”的方式就可以调用
方法:对象里面的函数称为方法,方法不需要声明,使用“对象.方法名()
”的方式就可以调用,方法用来描述该对象的行为和功能。
2. 静态成员和实例成员
我们知道,JS
中的一切皆是对象,那么构造函数其实也是个对象,同时通过new 构造函数
也可以创造出对象,我们一般也称这样的对象是实例。
function Cat(n, m){
this.name = n;
this.age = m;
this.climb = function(){
console.log('跑的真快');
}
}
var c1 = new Cat('猫', 3);
// 使用instanceof可以检测一个对象是否是另一个对象的实例
console.log(c1 instanceof Cat);
像上面的代码中,构造函数内使用this
开头设置的成员,在获得实例的时候,自动成为实例的成员。
// Cat 的构造函数
function Cat(n, m){
this.name = n;
this.age = m;
this.climb = function(){
console.log('跑的真快');
}
}
// 为构造函数对象动态添加成员(静态成员)
Cat.jump = function(){
console.log('我会跳');
}
var c1 = new Cat('猫', 3);
// 此时调用c1.jump()会报错
// c1.jump();
// 实例成员不能被构造函数对象调用
// Cat.climb();
// 为实例动态添加成员
c1.jump = function(){
console.log('跳的真高');
}
c1.jump();
Cat.jump();
// 对于一个实例动态添加的成员并不会被其它实例共有
var c2 = new Cat('黑猫', 2)
// c2.jump();
// Tiger 的构造函数
function Tiger(){
this.say = function(){
console.log('我是森林之王');
}
}
// 使用Cat的实例作为原型
Tiger.prototype = c2;
var t1 = new Tiger();
// 原型中有的成员也是实例成员
t1.climb();
通过上面的代码我们可以看出:
- 实例成员:构造函数中this上添加的成员和后期动态添加的成员
- 静态成员:构造函数本身添加的成员或者后期动态添加的成员
- 对于一个实例动态添加的成员并不会被其它实例共有
- 如果构造函数A有原型,那么构造函数A原型中的成员也是通过该构造函数A获取的实例的实例成员
通俗来讲:
实例成员:由构造函数创建出来的实例能直接访问的属性和方法,包括:实例本身的成员 以及 原型中的所有的属性和方法 和 后期动态添加的成员
静态成员:由构造函数直接访问到的属性和方法。
3. __proto__(原型)
、constructor(构造器)
和 prototype(原型类型)
在JS
中, __proto__
、constructor
和 prototype
三者关系是比较难搞懂的,为了搞懂它们,我们必须记住3个基本点:
prototype
属性是函数所独有的,其它对象没有。__proto__
和constructor
属性是对象独有,不过函数也是对象,因此函数也有这两个属性。__proto__
并不是JS
标准中规定的属性,不同的浏览器解释器实现的名字可能不一样,在ES标准中它叫[[Prototype]]
。
function Cat(){
}
var c1 = new Cat();
var o1 = {};
console.dir(Cat);
console.dir(c1);
console.dir(o1);
3.1 __proto__
属性
__proto__
属性 都是从一个对象指向一个对象,即指向它们的原型对象(可以理解为父对象)。它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__
属性所指向的那个对象(父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__
属性所指向的那个对象(爷爷对象)里找,依次类推,一直到最顶级对象,如果还找不到,那么就会报错。这个从当前对象一直到最顶级对象,通过__proto__
属性连起来的一条链,就是我们说的原型链。
3.2 prototype
属性
prototype
属性是函数独有的属性,它是从一个函数指向一个对象,表示函数的原型对象,通过new
这个函数获得到的实例对象的__proto__
属性都指向这个对象。
function Cat(){
this.describe = '猫';
}
var c1 = new Cat();
function Tiger(){
}
// 指定原型
Tiger.prototype = c1;
var t1 = new Tiger();
// 他们两个完全一样,就是同一个对象
console.log(t1.__proto__ === Tiger.prototype);
var t2 = new Tiger();
// 他们两个完全一样,就是同一个对象
console.log(t2.__proto__ === Tiger.prototype);
由上面的代码我们可以看出,其实prototype
属性其实主要是为了 共享(重用)成员(属性和方法),通过该函数得到实例,都能共享(重用)到这些成员。试想一下,如果每一次继承,都在内存中创建一份成员方法,那是多么的消耗内存啊。
3.3 constructor
属性
constructor
属性也是对象才拥有的(其实是__proto__
指向的对象或者prototype
指向的对象拥有的),它是从一个对象指向一个函数,含义就是指向该对象的构造函数,
,有一些特殊的属性的构造函数就是它自己本身。
// Cat
function Cat(){
this.describe = '猫';
}
var c1 = new Cat();
console.log(c1);
console.log(c1.constructor === Cat)
// Tiger
function Tiger(){
this.name = '老虎';
}
// 指定原型
Tiger.prototype = c1;
var t1 = new Tiger();
console.log(t1);
// 原型对象发生了覆盖
console.log(t1.constructor === Cat)
// NortheastTiger
function NortheastTiger(){
this.xxx = '东北虎';
}
// 指定原型
NortheastTiger.prototype = t1;
var n1 = new NortheastTiger();
console.log(n1)
// 原型对象发生了覆盖
console.log(n1.constructor === Cat)
每个对象都可以找到其对应的constructor
,因为创建对象的前提是需要有constructor
,而这个constructor
可能是对象自己本身显式定义的或者通过__proto__
在原型链中找到的,故通过函数创建的对象即使自己没有constructor
属性,它也能通过__proto__
找到对应的constructor
。
constructor
修正
上面的代码,可能很多人会在后面发生迷惑,我们看一下下面的代码:
// Cat
function Cat() {
this.describe = '猫';
}
var c1 = new Cat();
// Tiger
function Tiger() {
this.name = '老虎';
}
// Tiger.prototype 指向的对象的 constructor 指向构造函数本身,也就是Tiger
console.log(Tiger.prototype);
如果我们改变了Tiger.prototype
的指向,会造成原有的constructor
丢失:
// Cat
function Cat() {
this.describe = '猫';
}
var c1 = new Cat();
// Tiger
function Tiger() {
this.name = '老虎';
}
// Tiger.prototype 指向的对象的 constructor 指向构造函数本身,也就是Tiger构造函数
console.log(Tiger.prototype);
Tiger.prototype = c1;
// 此时 Tiger.prototype 指向的对象 是 c1 , c1的constructor执行的是自己的构造函数,也就是Cat构造函数
// 因此通过new Tiger() 获取到的对象的 constructor指向也为Cat构造函数
console.log(Tiger.prototype);
// t1的constructor指向Cat构造函数
var t1 = new Tiger();
console.log(t1.constructor);
很明显,这是有偏差的,我们可以手动将其修正:
function Cat() {
this.describe = '猫';
}
var c1 = new Cat();
// Tiger
function Tiger() {
this.name = '老虎';
}
// 从这里开始出现偏差
Tiger.prototype = c1;
// 修正一下
Tiger.prototype.constructor = Tiger
// 再获取的对象的constructor指向就正确了
var t2 = new Tiger();
console.log(t2.constructor);