JavaScript 高级程序设计(二)

P4 面向对象的程序设计

4.1 理解对象

// 创建实例
var person = new Object();
person.name = 'hqy';
person.age = 21;
person.sayName = function() {
    alert(this.name);
}

// 对象字面量
var person = {
    name: 'hqy',
    age: 21,
    sayName; function() {
        alert(this.name);
    }
}

数据属性

  • Configurable:能否通过 delete 删除属性,能否修改属性的特性,能否把属性修改为访问器属性,默认 true
  • Enumerable:能否通过for-in循环返回属性,默认 true
  • Writable:能否修改属性的值,默认 true
  • Value:这个属性的数据值,默认 undefined

使用ES5的 Object.defineProperty() 修改属性默认的特性

// 参数:属性所在对象,属性名,一个描述符对象
var person = {};
Object.defineProperty(person, 'name', {
    writable: false, // 设置成只读
    value: 'hqy'
})
alert(person.name) // hqy
person.name = 'sss'; // 严格模式下报错
alert(person.name) // hqy

// 将 configurable 设置成 false,delete 无效,严格模式报错
// 一旦把属性定义为不可配置的,就不能再把它变回可配置了,修改除 writable 之外的特性都会导致错误

访问器属性

  • Configurable:能否通过 delete 删除属性,能否修改属性的特性,能否把属性修改为访问器属性,默认 true
  • Enumerable:能否通过for-in循环返回属性,默认 true
  • Get:读取属性时调用的函数,默认 undefined
  • Set:写入属性时调用的函数,默认 undefined

访问器属性不能直接定义,必须使用 Object.defineProperty() 来定义

var book = {
    _year: 2004, // 下划线常用来表示只能通过对象方法访问的属性
    edition: 1
};
Object.defineProperty(book, 'year', {
    get: function() {
        return this._year;
    },
    set: function(val) {
        if(val > 2004) {
            this._year = val;
            this.edition += val - 2004;
        }
    }
})
book.year = 2005;
alert(book.edition) // 2

读取属性的特性
使用 Object.getOwnPropertyDescriptor(对象,属性名) 获得给定属性的描述符

4.2 创建对象-工厂模式

function createPerson(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function() {
        alert(this.name);
    }
    return o;
} 

缺点:没有解决对象识别的问题,即怎么知道一个对象的类型

4.2 创建对象-构造函数模式

// 构造函数始终都应该以一个大写字母开头
function  Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function() {
        alert(this.name);
    }
}
var person1 = new Person('hqy', 21);
var person2 = new Person('sss', 22);

与工厂模式相比:1. 没有显式创建对象, 2. 直接将属性和方法赋给this对象,3. 没有return
要创建Person的新实例,必须使用new

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(this指向这个新对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象

最后,person1person2 分别报错着 Person 的一个不同的实例,每个对象都有一个 constructor 属性,指向 Person

alert(person1.constructor == Person) // true
alert(person2.constructor == Person) // true

// 检测对象类型
alert(person1 instanceof Person) // true
alert(person1 instanceof Object) // true

将构造函数当作函数

// 当构造函数
var person = new Person('hqy', 21);
person.sayName() // hqy

// 当普通函数
Person('hqy', 21); // 添加到 window
window.sayName(); // 'hqy'

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'sss', 25);
o.sayName() // sss

缺点
每个方法都要在每个实例上重新创建一遍,即不同实例上的同名函数是不相等的

alert(person1.sayName == person.sayName) // false

解决办法

function  Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName() {
    alert(this.name);
}

但这样如果对象需要定义很多方法,那么就要定义这么多的全局函数。。。

4.2 创建对象-原型模式 ★

理解原型对象

  • 每个函数都有 prototype,指向函数的原型对象
  • 每个原型对象都有 constructor (构造函数),指向 prototype 属性所在函数的指针
  • Person.prototype.constructor 指向 Person
  • 每个对象可以用 __proto__ 访问 Prototype
  • 这个连接存在与实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间
image.png

确定对象间是否存在这种关系

// 通过 isPrototypeOf()
alert(Person.prototype.isPrototypeOf(person1)) // true

// Object.getPrototypeOf()
alert(Object.getPrototypeOf(person1) == Person.prototype) // true
alert(Object.getPrototypeOf(person1).name) // name 设置在原型上

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。

  • 搜索首先从实例开始。如果实例上找到了具有同名的属性。则返回该属性的值
  • 上面没有找到,继续搜索指针指向的原型对象,找到返回,没找到返回 undefined
image.png

不能通过对象实例重写原型中的值,也就是说实例上的属性怎么操作都不会影响到原型

function Person() {}
Person.prototype.name = 'hqy';
var person1 = new Person();
var person2 = new Person();
person1.name = 'sss';
alert(person1.name) // 'sss'
alert(person2.name) // 'hqy'

// 只有在实例上删除该属性才能重新访问到原型中的属性
delete person1.name;
alert(person1.name) // 'hqy'

hasOwnProperty() 方法检测一个属性是存在于实例上还是原型上,只在给定属性存在于实例上才会返回 true
in 单独使用时会在通过对象能够访问给顶属性时返回 true,无论在实例上还是原型中。

// 判断属性到底是存在于对象中还是实例上
function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object); 
}

枚举属性 Object.keys()Object.getOwnPropertyName()

function Person() {}
Person.prototype.name = 'hqy';
Person.prototype.age = 21;
Person.prototype.job = 'software engineer';

var key = Object.keys(Person.prototype);
alert(key) // name,age,job

var person = new Person();
person.name = 'bob';
var key = Object.keys(person);
alert(key) // name

var key = Object.getOwnPropertyName(Person.prototype);
alert(key) // constructor,name,age,job

更简单的原型语法

function Person() {}
Person.prototype = {
    constructor: Person
    // 以字面量创建,constructor 不再指向 Person
    name: 'hqy',
    age: 21,
    job: 'software engineer'
}

原型动态性

  • 重写整个原型对象,会切断构造函数与最初原型之间的联系
  • 实例中的指针仅指向原型,不指向构造函数
function Person() {}

var friend = new Person();

// 重写整个原型对象,会切断构造函数与最初原型之间的联系
Person.prototype = {
    constructor: Person
    // 以字面量创建,constructor 不再指向 Person
    name: 'hqy',
    age: 21,
    job: 'software engineer',
    sayName: function() {
        alert(this.name);
    }
}
friend.sayName(); // error
image.png

原生对象的原型

alert(typeof Array.prototype.sort) // function

String.prototype.getLength = function() {
    return this.length;
}

缺点

function Person() {}

Person.prototype = {
    constructor: Person
    name: [1, 2],   // 该属性会被实例共享
    sayName: function() {
        alert(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();

person1.push(3);
alert(person1.sayName); // 1,2,3
alert(person2.sayName); // 1,2,3
alert(person1.name === person2.name) // true

4.2 创建对象-构造函数+原型模式

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.friend = [1, 2];
}

Person.prototype = {
    constructor: Person
    sayName: function() {
        alert(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();

person1.push(3);
alert(person1.sayName); // 1,2,3
alert(person2.sayName); // 1,2
alert(person1.name === person2.name) // false

4.2 创建对象-动态原型模式

通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型

function Person(name) {
    // 属性
    this.name = name;
    //方法
    // 初次调用构造函数时才会执行
    if(typeof this.sayName !== 'function') {
        Person.prototype.sayName = function() {
            return this.name;
        }
    }
}

4.2 创建对象-寄生构造函数模式

寄生构造函数返回的对象与构造函数或者构造函数的原型属性之间没有关系,不能依赖 instanceof 确定对象类型。

// 创建一个具有额外方法的特殊数组
function SpecialArray() {
    var list = new Array();
    list.push.apply(list, arguments);
    list.toPipedString = function() {
        return this.join('|');
    }
    return list;
}
var color = new SpecialArray(1, 2, 3);
alert(color.toPipedString()) // 1|2|3

4.2 创建对象-稳妥构造函数模式

稳妥对象最适合在一些安全的环境中(这些环境禁止使用thisnew),或者在防止数据被其他应用程序改动时使用。

  • 新创建对象的实例方法不引用 this
  • 不使用 new 操作符调用构造函数
function Person(name) {
    var o = new Object();
    // 可以在这里定义私有变量和函数
    ...
    // 函数方法
    o.sayName = function() {
        alert(name);
    }
    return o;
}
var person = Person('hqy');
person.sayName() // hqy

除了调用 sayName(),否则没其他方法访问其数据成员

4.3 继承-原型链

构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

假如我们让原型对象等于另一个类型的实例:
此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型也包含着一个指向另一个构造函数的指针。
假如另一个原型有事另一个类型的实例,那么上面的关系依然成立,如此层层递进,就构成了实例和原型的链条,这就是所谓的原型链。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
}
function SubType() {
    this.subproperty = false;
}
// 继承 SubType
SubType.prototype = new SuperType();
SubType.protptype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
alert(instance.getSuperValue()) // true

instance 指向SubType 的原型,SubType 的原型又指向 SuperType 的原型。
getSuperValue() 方法仍然还在SuperType.protptype中,但property则位于SubType.prototype中。这是因为property是一个实例属性,而getSuperValue()是个原型方法。

image.png

确定原型和实例的关系

// instacnceof
alert(instance instanceof Object) // true
alert(instance instanceof SuperType) // true
alert(instance instanceof SubType) // true

// isPropertyOf()
alert(Object.prototype.isPropertyOf(instance)) // true
alert(SuperType.prototype.isPropertyOf(instance)) // true
alert(SubType.prototype.isPropertyOf(instance)) // true

谨慎地定义方法

  • 给原型添加方法一定能够要放在替换原型的语句之后
  • 在通过原型链实现继承时,不能使用对象字面量创建原型方法,这样做会重写原型链,切断构造函数和原型的联系

原型链的缺点
包含引用类型值的原型属性会被所有实例共享。(4.2 创建对象-构造函数+原型模式 缺点已介绍)

4.3 继承-借用构造函数

function SuperType(name) {
    this.name = name;
}
function SubType(name) {
    // 继承了 SuperType,同时传递参数
    SuperType.call(this, name);

    // 实例属性
    this.age = 21;
}
var instance = new SubType('hqy')
alert(instance.name); // hqy
alert(instance.age); // 21

缺点
仅仅是借用构造函数,方法都在构造函数中定义,因此函数复用就无从谈起了。

4.3 继承-组合继承(借用构造+原型链)

使用原型链实现对原型属性和方法的继承,而通过借用构造函数实现对实例属性的继承

function SuperType(name) {
    this.name = name;
    this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
    return 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() {
    return this.age;
}

var instance1 = new SubType('hqy', 21);
instance1.colors.push(4);
alert(instance1.colors) // 1,2,3,4
alert(instance.sayName()) // hqy
alert(instance.sayAge()) // 21

var instance2 = new SubType('sss', 25);
instance1.colors.push(4);
alert(instance1.colors) // 1,2,3
alert(instance.sayName()) // sss
alert(instance.sayAge()) // 2251

4.3 继承-原型式继承

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

object 函数内部,先创建了一个临时性的构造函数,然后将传入额对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

var person = {
    name: 'hqy',
    friends: [1, 2, 3, 4]
}
var a = object(person);
// 等价于 var a = Object.create(person);
a.friends.push(5);

var b = object(person);
a.friends.push(6);

alert(person.friends) // 1, 2, 3, 4, 5, 6
  • 返回的这个新对象将 person 作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。
  • 这意味着 person.friends 不仅属于 person 所有,而且也会被 ab 共享。
  • 包含引用类型值的属性始终都会共享相应的值

4.3 继承-寄生式继承

该函数在内部以某种方式来增强对象

fucntion createAnother(origin) {
    var clone = object(origin); // 创建一个新对象
    clone.sayHi = function() {  // 以某种方式来增强对象
        alert('hi');
    }
    return clone; // 返回这个对象
}

var person = {
    name: 'hqy',
    friends: [1, 2, 3, 4]
}
var a = createAnother(person);
a.sayHi(); // hi

4.3 继承-寄生组合继承

组合继承是 JavaScript 最常用的继承模式,但它最大的问题在无论什么情况下,都会调用两次超类型构造函数

  • 创建子类型原型时
  • 子类型构造函数内部

组合继承的例子

function SuperType(name) {
    this.name = name;
    this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
    return this.name;
}
function SubType(name, age) {
    // 继承属性
    SuperType.call(this, name); // 第二次调用 SuperType()
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor = SubType; // ★
SubType.prototype.sayAge = function() {
    return this.age;
}

寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
思路:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非就是超类型原型的一个副本而已

// 参数:子类型构造函数,超类型构造函数
function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype); // 创建超类型原型的一个副本
    prototype.constructor = subType; // 增强对象,为副本添加constructor属性
    subType.protptype = prototype; // 将副本复制给子类型的原型
}

function SuperType(name) {
    this.name = name;
    this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
    return this.name;
}
function SubType(name, age) {
    // 继承属性
    SuperType.call(this, name);
    this.age = age;
}
// 继承方法
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    return this.age;
}
image.png

P5 函数表达式

  • 函数声明提升,意思是在执行代码之前会项读取函数声明,但函数表达式只能当代码执行到它那一行后才会被解析。
  • 创建一个函数并把它赋值给变量 functionName,这种情况下创建的函数叫做匿名函数(拉姆达函数)

5.1 递归

arguments.callee 是一个指向正在执行的函数的指针

function factorial(num) {
    if(num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
        // 等价于 num * factorial(num -1),主要是为了防止函数名改变
    }
}

5.2 闭包

闭包:是指有权访问另一个函数作用域的变量的函数。
创建闭包最常见的方式,就是在一个函数内部创建另一个函数

function createComparisonFunction(key) {
    return function(obj1, obj2) {
        var val1 = obj1[key];
        var val2 = obj2[key];
        if (val1 < val2) {
            return -1;
        } else if(val1 > val2) {
            return 1;
        } else {
            return 0;
        }
    }
}

当一个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位。。。直至作为作用域链重点的全局执行环境。

作用域链本质上是一个指向变量对象的指针列表,它值引用当不实际包含变量对象。

在另一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域中,实际上将会包含外部函数createComparisonFunction()的活动对象。

image.png

createComparisonFunction() 函数执行完成后,其执行环境的作用域链会被销毁,但活动对象仍然留在内存中,知道匿名函数被销毁后,createComparisonFunction() 的活动对象才会被销毁。

var compareNames = createComparisonFunction('name') ;
var result = compareNames({ name: 'hqy' }, { name: 'sss' });

// 解除对匿名函数的引用,一遍释放内存
compareNames = null;

闭包和变量

function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function() {
            return i;
        }
    }
    return result;
}
// 每个函数都返回 10
// 因为每个函数的作用域中都保存着createFunction的活动对象,所以它们引用的是同一个 i

// 返回一个立即执行的匿名函数解决问题
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function(num) {
            return function() {
                return num;
            }
        }(i);
    }
    return result;
}

关于 this 对象
这里只是简单地介绍了下

var name = 'the window';

var object = {
    name: 'the object',
    getName: function() {
        return function() {
            return this.name;
        }
    }
}

// 内部函数在搜索 this, arguments 时,只会搜索到其活动对象为止
alert(object.getName()()) // 'the window' (非严格模式下)

要想获取 object 中的 name,这样做

var name = 'the window';

var object = {
    name: 'the object',
    getName: function() {
        var that = this;
        return function() {
            return that.name;
        }
    }
}
alert(object.getName()()) // 'the object'

几种特殊情况

var name = 'the window';

var object = {
    name: 'the object',
    getName: function() {
        return this.name;
    }
}

object.getName() // 'the object'
(object.getName)() // 'the object',和上面的代码定义是一样的
(object.getName = object.getName)() // 'the window' (非严格模式下),因为先执行的时赋值语句

内存泄漏

function handle() {
    var element = document.getElementById('someElement');
    var id = element.id; // 消除循环引用
    element.onclick = function() {
        alert(id);
    }
    element = null; // 解除对DOM对象的引用,顺利减少去引用数
}

5.3 模块模式-单例

指只有一个实例的对象

var singleton = {
    name: value,
    method: function() {
        // 这里是方法的代码
    }
}

模块模式通过为单例添加私有变量和特权方法能够使其得到增强

var singleton = function() {
    // 私有变量和私有函数
    var privateVal = 10;
    function privateFunc() {
        return false;
    }
    //特权/公有方法和属性
    return {
        publicProperty: true,
        publicMethod: function() {
            privateVal++;
            console.log(privateVal);
            return privateFunc();
        }
    }
}

var a = new singleton();
a.publicMethod(); // 11
a.publicMethod(); // 12
var b = new singleton();
b.publicMethod(); // 11

5.3 模块模式-增强的模块模式

var singleton = function() {
    // 私有变量和私有函数
    var privateVal = 10;
    function privateFunc() {
        return false;
    }

    // 创建对象
    var obj = new CustomType();

    //添加特权/公有方法和属性
    obj.publicProperty = true;
    
    obj.publicMethod = function() {
        privateVal++;
        console.log(privateVal);
        return privateFunc();
    }

    // 返回这个对象
    return obj;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容