关于ES6之面向对象的理解

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即接收到构造函数创建出来的对象。

注意事项:

  1. 构造函数约定首字母大写(建议这样操作)。
  2. 函数内的属性和方法前面需要添加 this ,表示当前对象的属性和方法。
  3. 构造函数中不需要 return 返回结果
  4. 当我们创建对象的时候,必须用 new 来调用构造函数

使用构造函数创建一个对象的过程:

  1. 在构造函数代码开始执行之前,创建一个空对象;

  2. 修改this的指向,把this指向创建出来的空对象;

  3. 执行函数的代码

  4. 在函数完成之后,返回创建出来的对象

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();

通过上面的代码我们可以看出:

  1. 实例成员:构造函数中this上添加的成员和后期动态添加的成员
  2. 静态成员:构造函数本身添加的成员或者后期动态添加的成员
  3. 对于一个实例动态添加的成员并不会被其它实例共有
  4. 如果构造函数A有原型,那么构造函数A原型中的成员也是通过该构造函数A获取的实例的实例成员

通俗来讲:

实例成员:由构造函数创建出来的实例能直接访问的属性和方法,包括:实例本身的成员 以及 原型中的所有的属性和方法 和 后期动态添加的成员

静态成员:由构造函数直接访问到的属性和方法。

3. __proto__(原型)constructor(构造器)prototype(原型类型)

JS中, __proto__constructorprototype三者关系是比较难搞懂的,为了搞懂它们,我们必须记住3个基本点:

  1. prototype 属性是函数所独有的,其它对象没有。
  2. __proto__constructor 属性是对象独有,不过函数也是对象,因此函数也有这两个属性。
  3. __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);
发布了22 篇原创文章 · 获赞 0 · 访问量 1146

猜你喜欢

转载自blog.csdn.net/bigpatten/article/details/103962065