资讯专栏INFORMATION COLUMN

温故知新之javascript面向对象

赵连江 / 2541人阅读

摘要:应该非常小心,避免出现不使用命令直接调用构造函数的情况。上面代码表示,使用属性,确定实例对象的构造函数是,而不是。当然,从继承链来看,只有一个父类,但是由于在的实例上,同时执行和的构造函数,所以它同时继承了这两个类的方法。

基本概念

类和实例是大多数面向对象编程语言的基本概念

类:类是对象的类型模板

实例:实例是根据类创建的对象
但是,JavaScript语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。了与普通函数区别,构造函数名字的第一个字母通常大写。

构造函数的特点有两个。

函数体内部使用了this关键字,代表了所要生成的对象实例。

生成对象的时候,必需用new命令

new与构造函数

new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。

下面两行代码是等价的。

var v = new Vehicle();
var v = new Vehicle;

应该非常小心,避免出现不使用new命令、直接调用构造函数的情况。为了保证构造函数必须与new命令一起使用,一个解决办法是,在构造函数内部使用严格模式,即第一行加上use strict

原理:由于在严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript不允许对undefined添加属性)。

new命令的原理

创建一个空对象,作为将要返回的对象实例

将这个空对象的原型,指向构造函数的prototype属性

将这个空对象赋值给函数内部的this关键字

开始执行构造函数内部的代码

即:

var obj  = {};
obj.__proto__ = Base.prototype;
Base.call(obj);

构造函数的return
如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象
这里遇到了一个问题,问题描述如下普通函数用new测试的时候箭头函数报错了

创建对象

JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。
当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined
例如,创建一个Array对象:
var arr = [1, 2, 3];
其原型链是:
arr ----> Array.prototype ----> Object.prototype ----> null
Array.prototype定义了indexOf()、shift()等方法,因此你可以在所有的Array对象上直接调用这些方法。
很容易想到,如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞得太长。

new Student()
function Student(name) {
    this.name = name;
    this.hello = function () {
        alert("Hello, " + this.name + "!");
    }
}
var xiaoming= new Student("xiaoming"),
    xiaohong= new Student("xiaohong");

xiaoming ↘
xiaohong -→ Student.prototype ----> Object.prototype ----> null
xiaojun ↗
用new Student()创建的对象还从原型上获得了一个constructor属性,它指向函数Student本身:

xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true

Object.getPrototypeOf(xiaoming) === Student.prototype; // true

xiaoming instanceof Student; // true
constructor
constructor属性的作用,是分辨原型对象到底属于哪个构造函数。
function F() {};
var f = new F();

f.constructor === F // true
f.constructor === RegExp // false
上面代码表示,使用constructor属性,确定实例对象f的构造函数是F,而不是RegExp。
构造函数继承 VS 原型链继承
xiaoming.name
//"xiaoming"
xiaohong.name
//"xiaohong"
xiaoming.hello
/*function() {
        alert("Hello, " + this.name + "!");
    }
*/
xiaohong.hello
/*function() {
        alert("Hello, " + this.name + "!");
    }
*/
xiaoming.hello === xiaohong.hello
//false

xiaoming和xiaohong各自的name不同,这是对的,否则我们无法区分谁是谁了。

xiaoming和xiaohong各自的hello是一个函数,但它们是两个不同的函数,虽然函数名称和代码都是相同的!

如果我们通过new Student()创建了很多对象,这些对象的hello函数实际上只需要共享同一个函数就可以了,这样可以节省很多内存。

要让创建的对象共享一个hello函数,根据对象的属性查找原则,我们只要把hello函数移动到xiaoming、xiaohong这些对象共同的原型上就可以了,也就是Student.prototype

修改代码如下:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    alert("Hello, " + this.name + "!");
};
xiaoming.hello === xiaohong.hello
//true
继承方式对比

借用构造函数继承
基本思想很简单,在子类型的构造函数内部调用父类型的构造函数:

function SuperType(){
 this.colors = ["red", "blue", "green"];
}
function SubType(){
     //继承了 SuperType
     SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

问题:方法都在构造函数内部定义,函数的复用性就无从谈起了。在超类型的原型中定义的方法,对子类型而言是不可见的。考虑这些问题,借用构造函数也是很少多带带使用。

组合继承
实现的思路是使用原型链实现对原型属性和方法的继承,而通过constructor stealing技术实现对实例属性的继承。

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
     alert(this.name);
};
function SubType(name, age){
     //继承属性
     SuperType.call(this, name);
     this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
     alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

组合继承避免了原型链和借用构造函数的缺陷,融合两者之长,是最常用的JS继承模式。

原型式继承
如果只是想让一个对象与另一个对象保持类似的情况下,没有必要兴师动众地创建构造函数。我们可以使用原型式继承。
Rect.prototype = Object.create(Shape.prototype);

原型继承
JavaScript的原型继承实现方式就是:

定义新的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this;

借助中间函数F实现原型链继承,最好通过封装的inherits函数完成;

继续在新的构造函数的原型上定义新方法。

var print = require("./print.js");

function Student(props) {
    this.name = props.name || "Unnamed";
}

Student.prototype.hello = function () {
    print("Hello, " + this.name + "!");
};

function Sub(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

Sub.prototype.getGrade = function() {
    return this.grade;
};

function inherits(Child, Parent) {
    var F = function() {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

inherits(Sub, Student);

var xiaoming = new Sub({
    name: "xiaoming",
    grade: 100
});

print(xiaoming.name + " " + xiaoming.grade);

print(xiaoming instanceof Student);
print(xiaoming instanceof Sub);

class继承
class Student {
    constructor(name) {
        this.name = name;
    }

    hello() {
        alert("Hello, " + this.name + "!");
    }
}
class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 记得用super调用父类的构造方法!
        this.grade = grade;
    }

    myGrade() {
        alert("I am at grade " + this.grade);
    }
}

ES6引入的class和原有的JavaScript原型继承有什么区别呢?实际上它们没有任何区别,class的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码。
这里遇到的问题是isPrototypeOf的问题

多重继承

JavaScript不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。

function M1() {
  this.hello = "hello";
}

function M2() {
  this.world = "world";
}

function S() {
  M1.call(this);
  M2.call(this);
}
S.prototype = M1.prototype;

var s = new S();
s.hello // "hello"
s.world // "world"
s instanceof M2
//false

上面代码中,子类S同时继承了父类M1和M2。当然,从继承链来看,S只有一个父类M1,但是由于在S的实例上,同时执行M1和M2的构造函数,所以它同时继承了这两个类的方法。

扩展

apply的应用:转换类似数组的对象

Array.prototype.slice.apply({0:1,length:1})
// [1]

Array.prototype.slice.apply({0:1})
// []

Array.prototype.slice.apply({0:1,length:2})
// [1, undefined]

Array.prototype.slice.apply({length:1})
// [undefined]

bind结合call,可以改写一些JavaScript原生方法的使用形式

[1, 2, 3].slice(0, 1)
// [1]

// 等同于

Array.prototype.slice.call([1, 2, 3], 0, 1)
// [1]

call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。

var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);

var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]

pop(a)
a // [1, 2, 3]

某个属性到底是原型链上哪个对象自身的属性。

function getDefiningObject(obj, propKey) {
  while (obj && !{}.hasOwnProperty.call(obj, propKey)) {
    obj = Object.getPrototypeOf(obj);
  }
  return obj;
}

获取实例对象obj的原型对象,有三种方法。

obj.__proto__
obj.constructor.prototype
Object.getPrototypeOf(obj)

推荐最后一种

面向对象感觉还没怎么搞明白,模块的东西还没弄明白,有时间补上

参考资料

廖雪峰老师的教程
阮一峰老师的教程
BruceYuj的博客

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92352.html

相关文章

  • SegmentFault 技术周刊 Vol.32 - 七夕将至,你的“对象”还好吗?

    摘要:很多情况下,通常一个人类,即创建了一个具体的对象。对象就是数据,对象本身不包含方法。类是相似对象的描述,称为类的定义,是该类对象的蓝图或原型。在中,对象通过对类的实体化形成的对象。一类的对象抽取出来。注意中,对象一定是通过类的实例化来的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 马上就要到七夕了,离年底老妈老爸...

    李昌杰 评论0 收藏0
  • SegmentFault 技术周刊 Vol.32 - 七夕将至,你的“对象”还好吗?

    摘要:很多情况下,通常一个人类,即创建了一个具体的对象。对象就是数据,对象本身不包含方法。类是相似对象的描述,称为类的定义,是该类对象的蓝图或原型。在中,对象通过对类的实体化形成的对象。一类的对象抽取出来。注意中,对象一定是通过类的实例化来的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 马上就要到七夕了,离年底老妈老爸...

    Lyux 评论0 收藏0
  • SegmentFault 技术周刊 Vol.32 - 七夕将至,你的“对象”还好吗?

    摘要:很多情况下,通常一个人类,即创建了一个具体的对象。对象就是数据,对象本身不包含方法。类是相似对象的描述,称为类的定义,是该类对象的蓝图或原型。在中,对象通过对类的实体化形成的对象。一类的对象抽取出来。注意中,对象一定是通过类的实例化来的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 马上就要到七夕了,离年底老妈老爸...

    AaronYuan 评论0 收藏0
  • js温故知新7(面向对象编程)——学习廖雪峰的js教程

    摘要:不区分类和实例的概念,而是通过原型来实现面向对象编程。新创建的的原型链是也就是说,的原型指向函数的原型。最后,创建一个对象代码和前面章节完全一样小明继承用定义对象的另一个巨大的好处是继承更方便了。 JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。 原型是指当我们想要创建xiaoming这个具体的学生时,我们并没有一个Student类型可用...

    Jaden 评论0 收藏0
  • 2017-09-27 前端日报

    摘要:前端日报精选是如何工作的内存管理如何处理个常见的内存泄漏译中的面向对象原型原型链继承源码事件机制考拉升级经验掘金中文第期你知道编译与解释的区别吗视频在白鹭引擎中的实践王泽变量自定义属性使用指南众成翻译禁止手机虚拟键盘弹出做 2017-09-27 前端日报 精选 JavaScript是如何工作的:内存管理 + 如何处理4个常见的内存泄漏(译) js中的面向对象、原型、原型链、继承Vue....

    wangym 评论0 收藏0

发表评论

0条评论

赵连江

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<