资讯专栏INFORMATION COLUMN

JavaScript面向对象那些事

王伟廷 / 1398人阅读

摘要:委托上面的代码结合了构造函数和原型两种方式去创建对象,首先聊聊构造函数构造函数构造函数本质上还是函数,只不过为了区分将其首字母大写了而已。注意注释掉的代码是自动执行的,但这并不是构造函数独有的,每个函数在声明时都会自动生成。

首先看看下面两个"1+1=2"的问题:

问题一:为什么改变length的值,数组的内容会变化?
var arr = [1];
arr.length = 3;
alert(arr);   // [1, undefined, undefined]
问题二:为什么在showScope函数内能访问outter,在函数外不能访问inner?
var outter = "sunshine";

function showScope() {
    var inner = "darkness";
    
    console.log(outter);    //"sunshine"
}

console.log(typeof inner)   // undefined

好了,接下来进入正文。

一、对象的属性
var person = {
    name: "Simon",
    _age: 21,
    isYoung: true,
    friends: ["Johnny", "Carlton", "Amy"],
    sayName: function() {
        console.log(this.name);
    }
    educate: {
        primarySch: "",
        highSch: "",
        university: ""
    }
};

上面的person对象是JS对象的字面量形式,本质上是一个键值对的无序集合,这些键值 对叫做属性。属性的名称只能是字符串形式的,而属性的值可以是字符串、数字、布尔值等基本类型,也可以是数组、函数、对象等引用类型。值得一提的是,如果属性的名称是JS能够识别的标识符,如name、first_name、$name,则在定义属性时不用像json那样为属性名加上引号;但属性名称是first-name这种JS无法识别的标识符时,就需要为其加上引号了。这两种情况也会造成访问方式不同,前者既可以通过person.first_name的形式访问,也可以通过person[first_name]的形式访问。但后者只能通过中括号的形式访问。

如果要对属性分类的话,属性可以分为两类:数据属性、访问器属性。这两种属性都分别有着一些特性:

数据属性

Configurable: 能否修改或删除属性,默认为true;

Enumerable: 能否通过for-in循环遍历属性,默认为true;

Writable: 能否修改属性的;

Value: 存放属性的值,默认为 undefined;

访问器属性

Configurable: 同上;

Enumerable: 同上;

Get: 在读取属性的值时调用的函数;

Set: 在设置属性的值时调用的函数;

这些特性无法直接访问,但可以通过Object.defineProperty(obj, attr, descriptor)函数定义这些特性。
基于上面的person对象各举一个例子:

// 数据属性
Object.defineProperty(person, "name", {
    configurable: false
})

console.log(person,name); // Simon
person.name = "zai";
console.log(person,name); // Simon

//访问器属性
Object.defineProperty(person, "age", {
    get: function() {
        return this._age;
    },
    set: function(newValue) {
    
        if (newValue > 30) {
            this._age = newValue;
            this.isYoung = false;
        }
    }
})

到这里第一个问题就得到了解决,数组的length属性其实就是一种访问器属性。

此外操作属性的方法还有:Object.defineProperties 用来一次定义多个属性,Object.getOwnPropertyDescriptor(obj, attr) 用来读取属性的特性。另外可以通过delete操作符去删除Configurable值为true的属性。

二、如何创建对象

仅仅通过字面量的方式去创建对象显然是不现实的,因为当我们需要创建多个相似的对象时,这样做会产生大量的重复代码。需要一种科学的方式去创建对象。

function Person(name, age, friends) {
    this.name = name;
    this.age = age;
    this.friends = friends;
    // this.prototype = { constructor: this };
}

Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log(this.name);
    }
}

Person.prototype.sayAge = function() {
    console.log(this.age);
};

var simon = new Person("Simon", 22, ["Amy", "Johnny", "Carlton"]);
simon.sayName();   //委托

上面的代码结合了构造函数和原型两种方式去创建对象,首先聊聊构造函数:

构造函数

构造函数本质上还是函数,只不过为了区分将其首字母大写了而已。注意注释掉的代码是自动执行的,但这并不是构造函数独有的,每个函数在声明时都会自动生成prototype。构造函数不一样的地方在于它的调用方式——new,new调用构造函数的大致过程:

产生一个新对象;

将构造函数的作用域赋给新对象;

执行构造函数中的代码;

返回新对象或者指定返回的对象;

构造函数本质上仍是函数,所以当然可以直接调用,这样构造函数中的this就指的是全局对象,显然不符合预期。

原型

《JavaScript高级程序设计》上的一幅图很好的解释了原型、构造函数、实例之间的关系:

执行simon.sayName( )时,首先在simon对象本身的作用域中寻找sayName,没有找到之后再去其原型Person.prototype中寻找,这个过程叫做委托。那么问题就来了,当我们不知道一个对象的构成时,如何去判断一个属性属于对象还是其原型呢?obj.hasOwnProperty(propName)就是做这个事情的函数,常常被用在for-in循环遍历对象的属性的过程中,与for-in类似的两个方法:Object.keys(obj)、Object.getOwnPropertyNames(obj) 这两个方法返回的都是属性名的数组,都不包括原型中的属性,区别在于前者和for-in一样只遍历enumrable为 true的属性,而后者遍历所有属性。

三、继承

这里给出一种JavaScript实现继承的方式:

function Vehicle(maxSpeed, wheels) {
    this.maxSpeed = maxSpeed;
    this.wheels = wheels;
}

Vehicle.prototype.checkMaxSpeed = function() {
    console.log(this.maxSpeed);
};

function Car(brand, maxSpeed) {
    Vehicle.call(this, maxSpeed, 4);
    this.brand = brand;
}

Car.prototype = new Vehicle();
Car.prototype.constructor = Car;
Car.prototype.checkBrand = function() {
    console.log(this.brand);
};

var panemera = new Car("Panemera", 250);

这里的关键在于在Car中调用Vehicle,向父类构造器传递参数,初始化子类的属性,再进行扩充(brand),当然仅仅有构造函数还是不行的,还需要原型链才能更好地实现继承,这里Car的原型是Vehicle的一个实例,值得注意的是Car.prototype = new Vehicle();之后,原本的constructor丢失了,新的constructor在这里指向了Vehicle,需要重置为Car。

之前提出的第二个问题其实就是用继承来实现的:

function showScope() {
    // scope代表当前作用域
    var oldScope = scope;
    var Scope = function() {};
    
    //继承当前作用域
    Scope.prototype = scope;
    scope = new Scope();
    
    // 进入函数作用域,扩充作用域
    advance("{");
    parse(scope);     // 用当前作用域做解析
    advance("}");
   
   scope =oldScope;
}

假设showScope是解析作用域的函数,它的实现机制大概是:进入函数作用域之前保存当前作用域,新建一个继承了当前作用域的对象并用它取代当前作用域,解析左括号进入函数作用域并对当前作用域进行扩充,使用扩充后的作用域进行解析,解析右括号离开函数作用域,恢复进入函数前的作用域。

四、私有成员的实现

最后说说JavaScript中私有成员的实现,一个很有趣的例子:

function AladdinLamp() {
    var limit = 3;
    
    function rubLamp() {
        if (limit > 0) {
            limit -= 1;
            return true;
        } else {
            return false;
        }
    }
    
    this.satisfyWish = function() {
        return rubLamp() ? Math.random() : null;
    };
}

这里的limit和rubLamp都是AladdinLamp的私有成员,无法从外部直接访问,只能通过唯一暴露出来的satisfyWish调用,这实际上是一种闭包,关于闭包请参考本专栏中的浅谈JavaScript中的闭包

五、ES6中的类与继承

上文谈到的都是ES5,那么ES6有什么不同呢,先来看看ES6中的类:

class Vehicle {
    constructor(maxSpeed, wheels) {
        this.maxSpeed = maxSpeed;
        this.wheels = wheels;
    }
    
    checkMaxSpeed() {
        console.log(this.maxSpeed);
    }
    
    static openDoor() {
        console.log("Welcome");
    }
}

Vehicle.length = 100;

let bike = new Vehicle(40, 2);
// TypeError
bike.openDoor();

不同之处在于构造函数换成了Class,其实Class本质上也是函数,constructor就相当于ES5中的构造函数,而直接在类中声明的checkMaxSpeed实际相当于 Vehicle.prototype.checkMaxSpeed = ...
有意思的是ES6中多了静态方法的实现,这里的openDoor无法在实例中调用,可以通过Vehicle.openDoor直接调用,可以继承给子类。另外通过Vehicle.props = ...的形式可以定义静态变量。最后注意Vehicle只能通过new调用,否则会报错,是因为在constructor中检测了new.target。
再看看ES6中的继承:

class Car extends Vehicle {
    constructor(maxSpeed, wheels, brand) {
        super(maxSpeed, wheels);
        this.brand = brand;
    }
    
    checkBrand() {
        console.log(this.brand);
    }
}

继承的关键在于constructor中调用了super,即父类的构造函数。这里一定要调用super,因为子类的this是由super创建的,之后再去扩充this。

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

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

相关文章

  • 【面试】Java基础的那些-Two

    摘要:前言面试中对于技术职位,一般分笔试与面谈,如果面试官的一些小问题你可以立马找到对应的知识点扩展开来,那么这就是你的优势,本系列将讲述一些面试中的事,不会很详细,但是应该比较全面吧。 前言 面试中对于技术职位,一般分笔试与面谈,如果面试官的一些小问题你可以立马找到对应的知识点扩展开来,那么这就是你的优势,本系列将讲述一些java面试中的事,不会很详细,但是应该比较全面吧。 主要内容 pa...

    you_De 评论0 收藏0
  • js面向对象浅析--继承那些

    摘要:有需要还可以修改指向谦龙寄生组合式继承思路是通过借用构造函数来继承属性,通过原型链的混合形式来继承方法改变执行环境实现继承有需要还可以修改指向谦龙谦龙拷贝继承该方法思路是将另外一个对象的属性和方法拷贝至另一个对象使用递归 前言 js中实现继承的方式只支持实现继承,即继承实际的方法,而实现继承主要是依靠原型链来完成的。 原型链式继承 该方式实现的本质是重写原型对象,代之以一个新类型的实例...

    molyzzx 评论0 收藏0
  • 【Java猫说】类与对象那些

    摘要:也可以这么说,对象就好像通讯簿中的一笔数据。对象有已知的事物,并能执行工作。对象本身已知道的事物成为实例变量,它代表对象的状态。对象可执行的动作称为方法,它代表对象的行为。 阅读本文约2.1分钟。 当你在设计类时,要记得对象时靠类的模型塑造出来的,你可以这样看: ——对象是已知事物 ——对象会执行的动作 对象本身已知的事物称为实例变量,它们代表对象的状态(数据),且该类型的每一个对象...

    BWrong 评论0 收藏0
  • 继承的那些

    摘要:借用构造函数继承针对上面的继承方法的缺点,开发人员使用一种叫做借用构造函数的技术,也就是我们平时说的跟继承。 继承是 OO 语言中一个最为津津乐道的概念,许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承而且实现继承主要是依靠原型...

    CoyPan 评论0 收藏0
  • python编程分析了一下高考那些,发现了两个之最,原来是这样

    摘要:它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。代码这是自年恢复高考以来到年的高考报考及录取数据。为了直观展示,对录取率做了尺度上的变换。 Python(发音:英[?pa?θ?n],美[?pa?θɑ:n]),是一种面向对象、直译式电脑编程语言,也是一种功能强大的通用型语言,已经具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常...

    Jingbin_ 评论0 收藏0

发表评论

0条评论

王伟廷

|高级讲师

TA的文章

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