前端社团Web前端之路让前端飞

JavaScript 高级程序设计(二)

2017-07-02  本文已影响95人  淡就加点盐

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

数据属性

使用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 之外的特性都会导致错误

访问器属性

访问器属性不能直接定义,必须使用 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

最后,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 创建对象-原型模式 ★

理解原型对象

image.png

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

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

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

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

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),或者在防止数据被其他应用程序改动时使用。

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

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 函数表达式

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;
}
上一篇 下一篇

猜你喜欢

热点阅读