《JavaScript高级程序设计》(第三版)学习复盘(一)

303 阅读24分钟

序言:前段时间面试的时候,面试官问到的问题其实很多在本书都有,自己看了,忘了。回国头来才发现,根本没有对本书完全掌握。 所以本篇文章是对本书的知识复盘。

!注意:本文章用的是ECMAScript5语法。

一、数据类型

ECMAScript中,有五种基本类型:Undefined,Null,Boolean,Number,String一种复杂数据类型(也称之为引用类型):object。里面包含的 function、Array、Date。

1.typeof操作符

typeof null // "object"。

特殊值null被认为是一个空的对象引用,它表示一个空的对象指针,所以typeof返回的是object。

从技术角度来讲,函数在ECMAScript中是对象。

2.Number类型。

这种数据类型使用IEEE754格式表示整数和浮点数。

1)浮点数值

浮点数最高精度是17为小数,但在计算其精度远远不如整数。例如:0.1 + 0.2的结果不是0.3,而是0.30000000000000004.这个误差会导致无法测试特定的浮点数值。

例如:

if(a + b === 0.3 ) { //  不要这样做
    alert("you got 0.3") 
}

这里使用的是IEEE754数值的浮点数计算的诟病。

2)数值转换

有三个函数可以把非数值转化为数值:Number()、parseInt()和parseFloat()。第一个函数可以转化任何数据类型,而后两个专门用于字符串转化成数值。

Number()转换规则如下:

如果是Bollean,true个false将分别转换为1和0. 如果是数字,知识简单的输入和返回。 如果是null,返回的是0。 如果是undefined,返回的是NaN。 如果是字符串: 1.字符串只包含数字,则转换为 10进制数字,如 "123" => 123,"011" => 11. 2.如果是浮点数,如 "01.1" => 1.1 . 3.字符串是16进制,如"0xf" => 15. 4.空字符串为0. 5.包含除上述以为的字符串,为NaN。

例如:

var num = Number("hello word!") // NaN
var num = Number("") // 0
var num = Number("00011") // 11
var num = Number("true") // 1

parseInt()可以传入第二个参数,用来 转换多少进制

var num = parseInt("AF", 16) // 175

3.操作符

var num1 = 2;
var num2 = 22;
var num3 = --num1 + num2; //  21
var num4 = num1 + num2; // 21

运算符在前面就立即执行

var num1 = 2;
var num2 = 22;
var num3 = num1=- + num2; //  22
var num4 = num1 + num2; // 21

运算符在后面在包含他们的语句被求值之后在执行。

隐示转换

var s1 = "01";
var s2 = "1.1";
var s3 = "z";
var b = false;
var f = 1.1;
var o = {
    valueOf: function () {
        return -1;
    }
}

s1 = +s1;  // 1
s2 = +s2;  // 1.1 
s3 = +s3;  // NaN
b = +b;  //  0
f = +f;  //  1.1
o = +o;  // -1

var result1 = ("55" == 55) //  true,先转换在比较。
var result2 = ("55" === 55) //  false,因为数据类型不相等。

4.for-in 语句

for-in语句是一种精准迭代语句,可以用来枚举对象属性(可以枚举原型对象)。

5.break和continue

break和continue语句都用于在循环中精确控制代码执行;break语句会立即退出循环,强制执行循环后面的语句continue语句也是立即退出循环,但是循环退出后会从循环顶部继续执行

6. 函数没有重载

ECMAScript中定义两个相同的函数名字的函数,则改名字属于后定义的函数。

例如:

funtion addNum(num) {
    return num + 100
}
funtion addNum(num) {
    return num + 200
}
var result = addNum(100); //  300

二. 变量与作用域

1.复制变量值

基本类型: 一个变量想另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把改值复制到新变量分配的位置上,(基本类型的值是储存在栈内存当中)。

引用类型: 当从一个变量向另一个变量复制引用类型的值时,同样也会在变量对象中复制一份放到为新变量分配的空间中。不用的是,这个复制的副本实际上是一个指针,而这个指针指向存储在队内存中的一个对象。复制操作之后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量,(引用类型的数据保存在堆内存当中,复制引用类型的值复制的其实是指针)。

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name); //  "Nicholas"

2.传递参数

ECMAScript中参数都是按值传递的,基本类型的传递,如同基本类型的变量复制是一样的,参数实际上是函数的局部变量。 例如:

function add(num){
    num += 10;
    return num;
}
var count = 20;
var result = add(count);
console.log(count); //  20 没有变化
console.log(result);  //  30

对象传参和基本类型有些区别,例如:

functionsetName(obj){
    obj.name = "Nicholas";
}
var person = new Object();
setName(person);
console.log(person.name); //  "Nicholas"

在这个函数内部,obj和person都是引用的同一个对象,即这个变量是按值传递的,obj也会引用访问同一个对象。

在看一个例子:

functionsetName(obj){
    obj.name = "Nicholas";
    obj =  new Object(); //  重写变量
    obj.name = "Greg";
}
var person = new Object();
setName(person);
console.log(person.name); //  "Nicholas"

当函数重写obj时,这个引用就是局部变量对象了,而这个局部变量会在函数执行完毕之后立即销毁。

3.数据类型检查

typeof 能满足检查一切基本类型的数据,但是在处理引用类型数据就捉襟见肘了,例如:

console.log(typeof null) //  object
consele.log(typeof {})  //  object

在检测运用类型的时候我们可以用instanceof来检测。例如:

console.log(['1','2'] instanceof Array); // true;
console.log({} instanceof Object); //  true;

4.作用域

在ECMAScript总作用域只有两种全局作用域和局部作用域。全局作用域也被认为是window对象,全局环境只能访问全局环境的变量和函数,不能访问局部变量中的任何数据。但是在可以用一些方法来延长作用域。在执行下列任何一个语句的时,作用域链会得到加长:

  • try-catch语句中的catch块;
  • with语句。 例如:
function buildUrl(){
    var qs = "?debug=true";
    with(location){
        var url = href + qs;
    }
    return url;
}

5.垃圾回收

局部变量只在函数执行过程中存在,在这期间会分配内存空间来存储她们的值,直到函数执行结束,这时候局部变量就没有存在的必要了,可以释放内存以供将来使用。浏览器实现内存回收一共有两种策略:

1)标记清除最常用的垃圾回收策略

2)计数清除不太常见的垃圾回收策略

数据不再有用,最好将其值设置为null来释放其引用,这个做法叫做解除引用。这个做法适用于大多数全局变量和全局对象的属性。局部变量也会在它们离开执行环境时自动解除引用,解除引用不是自动回收占用内存,而是让值脱离执行环境,以便垃圾收集器下次运行的时将其回收。

三.引用内型

1.Array 类型

检测数组方法:

1)typeof 方法

var arr = [1,2,3,4];
console.log(typeof arr); //输出结果是Object,会有一定的问题,不建议使用。

2)instanceof方法

var arr = [1,2,3,4];
console.log(arr instanceof Array)  //true

3)obj.constructor 方法

var arr = [1,2,3,4];
console.log(arr.constructor === Array) //true

4)Object.prototype.toString.call(obj) 方法

var arr = [1,2,3,4];
function isArray(obj){
    return Object.prototype.toString.call(obj) === '[object Array]';
}
console.log(isArray(arr));

5)Array.isArray(obj)方法

var arr = [1,2,3,4];
console.log(Array.isArray(arr)); //  true

数组操作:

arr.push() //  可以接收任意数量的参数,把他们逐个添加到数组的末尾,返回修改后数组的长度
arr.pop() //  从数组末尾移除最后一项,返回移除的项
arr.shift() //  移除数组的第一项并返回该项
arr.unshift() //  向数组前端添加任意个项并返回新数组的长度
arr.concat( arr1 || a,b,c) //  基于当前数组创造一个新的对象,,并合并数组,可以用于深拷贝数组
arr.slice(起始位置, 结束位置)  //  基于当前数组创造一个新的对象,切分数组,新数组不包含结束位置的项,可以深拷贝数组
arr.splice(起始位置, 结束位置,插入元素)  //  删除|插入|替换数组,删除:可以删除的任意数量的项接受两个参数如:splice(0,2);插入:可以指定位置插入任意数量的如:splice(2,0,"red","block");替换:可以指定位置插入任意数量的项,插入可以和删除的数量不相等,如:splice(2,1,"red","block")意思是删除位置为2的项,在从删除的位置插入“red”,“block”;
//  slice和concat这两个方法,仅适用于对不包含引用对象的一维数组的深拷贝

数组排序:

1)arr.sort()方法

只接收两个参数

var arr = [0,1,5,10,15];
arr.sort(); //  会先调用toString()方法,在排序
console.log(arr);  // [0,1,10,15,5] 
function compare(a,b) {
    if (a < b) {
        return -1;
    } else if (a > b) {
        return 1;
    } else {
        return 0;
    }
}
arr.sort(compare);
console.log(arr);  // [0,1,5,10,15]

2)arr.reverse()方法

var arr = [0,1,5,10,15];
arr.reverse();
console.log(arr); // [15,10,5,1,0];

数组迭代:

1)every()

every:对数组中的每一项运行的给定函数,如果该函数对每一项都返回的是true,则返回ture,反之false。

var numArr = [1,2,3,4,5,6,7];
var everyFun = numArr.every(function(item){
    return item > 2;
});
console.log(everyFun) // fasle;

2)some()

some:对数组中的每一项运行的给定函数,如果该函数任意一项返回的是true,则返回ture,反之false。

var numArr = [1,2,3,4,5,6,7];
var someFun = numArr.some(function(item){
    return item > 2;
});
console.log(someFun) // true;

3)filter()

filter:对数组中每一项运行给定函数,返回该函数会返回true的项的数组。

var numArr = [1,2,3,4,5,6,7];
var filterFun = numArr.filter(function(item){
    return item > 2;
});
console.log(filterFun) // 3,4,5,6,7;

4)forEach()

forEach:对数组每一项运行给定函数,该方法无返回值

var numArr = [1,2,3,4,5,6,7];
var forEachFun = numArr.forEach(function(item){
    console.log(item) // 1,2,3,4,5,6,7;
});

5)map()

map:对数组每一项运行给定函数,返回每次函数调用返回结果组成的数组

var numArr = [1,2,3,4,5,6,7];
var mapFun = numArr.map(function(item){
    return item + 1;
});
console.log(mapFun) // 2,3,4,5,6,7,8

每个迭代方法都会有三个参数依次是(item, index, array); item: 数组项的值;index:该项在数组中的位置;array: 数组本身

数组归并方法:

归并方法中可以代四个参数分别是(prev,cur,index,array);prev:前一项;cur:当前项;index:项的索引;array: 数组本身;

1)reduce()

reduce: 从第一项开始,逐个遍历到最后。

2) reduceRigth()

reduceRigth: 从最后一项开始,逐个遍历到第一项。

var numArr = [1,2,3,4,5];
var sum = numArr.reduce(function(prev,cur,index,array){
    return prev + cur;
});
var sum1 = numArr.reduceRigth(function(prev,cur,index,array){
    return prev + cur;
});
console.log(sum); //  15
console.log(sum1); //  15

2.Date对象

1.new Date()

可以接收日期格式的字符串,也可以不传递参数取得当前时间

2.Date.parse()

接收一个表示日期的字符串参数,然后返回相对应的日期的毫秒数。

//  2001年5月25日
var someData = new Data(Data.parse("May 25, 2001"));

3.Data.UTC()

接受多个参数,分别是“年份”、“月份”、“月份中的某天”、“时”、“分”、“秒”,前两个参数是必传值。

//  2005年4月5日11点22分33秒
var fives = new Data(Data.UTC(2005,4,5,11,22,33))

4.Date.now()

返回调用这个方法的日期时间的毫秒数,使用 +new Date()也可以得到相同的效果

3.Function 函数

1)函数没有重载

声明一个函数为 fun1,在创建一个相同的函数fun1的时候实际上是覆盖了第一个函数的声明变量。

2)函数声明和函数表达式

// 函数声明
    console.log(sum(10,10)) //  20
    function sum (a, b) {
        return a + b;
    }

函数声明会在代码执行之前解析器就已经通过一个名为函数声明提升的过程读取并将函数声明添加到执行环境中。对代码求值时,JavaScript在第一遍声明函数的时候将他们放到源代码树的顶部。

// 函数表达式
    console.log(sum(10,10)) //  sum is not a function
    var sum = function (a, b) {
        return a + b;
    }

函数表达式,在初始化中,变量sum并没有存储函数的引用,在第一行代码就会报错。

3)函数内部属性

1.arguments

arguments是函数内部的一个类数组对象,包含传入的所有参数; 其中arguments对象有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数 arguments.callee() //调用函数自身,在严格模式下运行时会导致错误

2.this

this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的。(谁调用谁负责)

3.caller

返回函数的执行环境,如果函数的执行环境为window则返回null,例子如下:

function func() {
    console.log(func.caller);
}
function obj() {
    func(); //  调用者为obj函数
}
obj();
//返回   ƒ obj() {
//     func(); //  调用者为obj函数
//   }
func(); //  返回 null

4.函数属性和方法

1)apply()

apply()接收两个参数:第一个是在其中运行函数的作用域,第二个是Array的实例,也可以是arguments对象。例如:

function sum (a,b) {
    return a + b;
}
function callSum1 (a,b) {
    return sum.apply(this, arguments);
}
function callSum2 (a,b) {
    return sum.apply(this, [a,b]);
}
console.log(callSum1(10, 10)); //  20
console.log(callSum2(10, 10)); //  20

2)call()

call()接收两个参数:第一个是在其中运行函数的作用域,第二个是传递的参数需要一一列出。例如

function sum (a, b) {
    return a + b;
}
function callSum (a, b) {
    return sum.apply(this, a, b);
}
console.log(callSum (10, 10)); //  20

call 和 apply 最主要的用法是扩展函数懒以运行的作用域(也就是为了动态改变 this 而出现的)

3) bind()

bind()该方法会创 建一个函数的实例,其this值会被绑定到传给bind()函数的值

window.color = "red";
var o = {
    color: "blue"
};
function sayColor () {
    console.log(this,color)
}
var objSayColor = sayColor.bind(o);
objSayColor(); //   blue

bind是静态方法只有粘贴的作用不会立即执行函数,call 和 apply则反之。

bind不会破坏原宿住的任何东西,所以宿住的内容改变,bind返回的结果也会改变。

bind会返回一个新的函数。

4)Math对象

Math.max() //  求数组中的最大值
Math.min() // 求数组中的最小值
// 求数组中最大值 Math.max.apply(Math, arr)
Math.ceil() //向上,四舍五入
Math.floor() //向下,四舍五入
Math.round()  //标准四舍五入
Math.random()  //返回大于等于0小于1的随机数
Math.abs() //返回参数的绝对值
Math.pow(num,power) // 返回num的power次幂
Math.sqrt(num) // 返回num的平方根

5)Number

var num = 10.0005
console.log(num.toFixed(2)); //  返回两位小数的数值字符串

四.面向对象的程序设计

1.属性类型

ECMAScript中有两种属性:数据属性和访问器属性。要修改默认属性,必须使用ECMAScript的Object.defineProperty()

1)数据属性

数据属性包含一个数据的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性

var person = {};
Object.defineProperty(person, "name", {
    configurable: true,  // 表示能否通过delete删除属性从而重新定义属性,能否修改属性,或者能否吧属性修改为访问器属性。
    enumerable: true,  // 表示能否通过for-in循环返回属性
    writable: true,  // 表示是否能修改属性的值
    value: "Nichonlas" // 包含这个属性的值
})
console.log(person.name) //  "Nichonlas"

2)访问器属性

访问器属性不包含数据值,它包含一个getter和setter函数(不过两个函数不是必须的)。

var book = {edition: 1, year: 2004};
Object.defineProperty(book, "year", {
    configurable: true,
    enumerable: true,
    get: function(){
        return this.year
    },
    // 定义Set时,则设置一个属性的值时会导致其他属性发生变化
    set: function(newValue){
        this.year = newValue;
        this.edition += newValue - 2004;
    }
})
book.year = 2005;
console.log(book.edition); //  2

3)定义多个属性

ECMAScript5中定义了一个Object.defineProperties()方法。利用这个方法可以用过描述一次性定义多个属性。这个方法接受两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与要与第一个对象中的要一一对应。例如:

var book = {};
Object.defineProperties(book, {
    _year: {
        writable: true,
        value: 2004
    },
    edition: {
        writable: true,
        value: 1
    },
    year: {
        get: function() {
           return this._year; 
        },
        set: function(newValue) {
            if( newValue > 2004 ) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
})

4) 读取属性的特性

ECMAScript5中定义了一个Object.getOwnPropertyDescrior()方法,可以取得给定属性的描述符。这个方法接受两个参数:属性所在的对象和要读取其描述符的属性名称。例如:

var book = {};
Object.defineProperties(book, {
    _year: {
        value: 2004
    },
    edition: {
        value: 1
    },
    year: {
        get: function() {
           return this._year; 
        },
        set: function(newValue) {
            if( newValue > 2004 ) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
})
var descriptor = Object.getOwnPropertyDescrior(book, "_year");
console.log(descriptor.value); // 2004
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // undefined

var descriptors = Object.getOwnPropertyDescrior(book, "year");
console.log(descriptors.value); // undefined
console.log(descriptors.enumerable); // false
console.log(typeof descriptor.set); // function

2.创建对象

1)工厂模式

返回新的对象。

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function () {
        console.log(this.name)
    };
    return o;
};
var person1 = createPerson("小明", 29, "工人");
var person2 = createPerson("小强", 20, "学生");

2)构造函数模式

创建一个构造函数,必须使用 new 操作符。以这种方式调用的构造函数实际上会经历以下4个步骤:

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

而且创造的构造函数都是Object的实例。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name)
    };
};
var person1 = new Person("小明", 29, "工人");
var person2 = new Person("小强", 20, "学生");

在任何函数使用 new 来调用的,那它就可以作为构造函数

构造函数的缺点: 每个方法在实例方法中都要重新创建一遍function实例。 例如:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("console.log(this.name)"); //  与声明函数是等价的
};
var person1 = new Person("小明", 29, "工人");
var person2 = new Person("小强", 20, "学生");
console.log(person1.sayName == person2.sayName); //  false;

所以没有必要创建两个Funcrion实例 而且两个实例中的执行代码也是一摸一样。因此,我们可以通过下面的方法来解决问题:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
};
function sayName() {
    console.log(this.name)
};

3) 原型模式

原型模式的属性都是所有实例共享的。

function Person() {};
Person.prototype.name = "小明";
Person.prototype.age = 29;
Person.prototype.job = "工人";
Person.prototype.sayName = function (){
    console.log(this.name)
}
var person1 = new Person();
person1.sayName(); //  小明
var person2 = new Person();
person2.sayName(); //  小明
console.log(person1.sayName == person2.sayName); //  true;

1.理解原型对象

无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性的指向函数的原型对象。在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。

函数在构造之后,其原型对象默认取得constructor属性。当构造函数实例化之后,该对象会包含一个指针,指向构造函数的原型对象。在ECMAScript5中管这个指针叫做[[ prototype ]],在脚本中没有标准的访问[[ prototype ]],但是在一些浏览器上都支持一个属性_proto_;这个属性存在于实例与构造函数的原型对象之间,而不存在于实例于构造函数之间

2.原型判断方法

function Person() {};
Person.prototype.name = "小明";
Person.prototype.age = 29;
Person.prototype.job = "工人";
Person.prototype.sayName = function (){
    console.log(this.name)
}
var person1 = new Person();
var person2 = new Person();
isPrototypeOf()方法
//  isPrototypeOf()方法
console.log(Person.prototype.isPrototypeOf(person1)); //  true
console.log(Person.prototype.isPrototypeOf(person2)); //  true
getPrototypeOf()方法
//  getPrototypeOf()方法,可以方便的读取一个对象的原型,只有属性存在于实例中,菜返回true。
console.log(Object.getPrototypeOf(person1) == Person.prototype); //  true
console.log(Object.getPrototypeOf(person1).name); //  小明
hasOwnProperty()方法
// hasOwnProperty()方法可以检测一个属性是在实例中,还是存在于原型中
console.log(person1.hasOwnProperty("name")); // false 来自原型

person1.name = "小红";
console.log(person1.name); // 小红
console.log(person1.hasOwnProperty("name")); // ture 来自实例

console.log(person2.name); // 小明
console.log(person2.hasOwnProperty("name")); // false 来自原型

delete person1.name;
console.log(person1.name); // 小明
console.log(person1.hasOwnProperty("name")); // false 来自原型

我们在实例中添加一个属性,而该属性与实例原型中的一个属性同名,在对象中就创建该属性,并且屏蔽原型中的那个相同的属性,而用delete操作符可以完全删除实例属性,能重新访问到原型属性

in操作符
//  in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在实例中还是原型中。
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true

person1.name = "小红";
console.log(person1.name); // 小红
console.log(person1.hasOwnProperty("name")); // ture 来自实例
console.log("name" in person1); // true

in操作符只要通过对象访问到属性就返回true,hasOwnProperty()方法只有属性存在于实例中才返回true,因此只要in操作符返回true而hasOwnProperty()返回false,就可以确定是原型中的属性。例如;

function hasPrototypeProperty(obj, name) {
    return !obj.hasOwnProperty(name) && (name in obj);
}

function Person() {};
Person.prototype.name = "小明";
Person.prototype.age = 29;
Person.prototype.job = "工人";
Person.prototype.sayName = function (){
    console.log(this.name)
}
var person = new Person();
console.log(hasPrototypeProperty(person, "name")); // true;

person.name = "小红";
console.log(hasPrototypeProperty(person, "name")); // false;
Object.keys()方法

返回调用的对象的属性(可枚举类型)。

function Person() {};
Person.prototype = {
    name: "小明",
    age: 29,
    job: "工人",
    sayName: function (){
        console.log(this.name)
    }
}
var keys = Object.keys(Person.prototype);
console.log(keys); // "name, age, job, sayName"

var person = new Person();
person.name = "小红";
parson.age = 20;
var parsonKeys = Object.keys(parson);
console.log(parsonKeys); // name,age
Object.getOwnPropertyNames()方法

获取所有实例属性(无论可不可以枚举)。

function Person() {};
Person.prototype = {
    name: "小明",
    age: 29,
    job: "工人",
    sayName: function (){
        console.log(this.name)
    }
}
var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "constructor, name, age, job, sayName"

3.原型的动态性

function Person() {};
var person = new Person();
Person.prototype = {
    name: "小明",
    age: 29,
    job: "工人",
    sayName: function (){
        console.log(this.name)
    }
};
person.name(); // error

如上述例子。尽管可以随时为原型添加属性和方法,但是重写整个原型对象就不一样了。调用构造函数的时会为了实例添加一个最初的原型指针[[ prototype ]],那么重写整个原型对象就等于切断了构造函数与最初的原型之间的联系。那么 实例中的指针仅仅指向原型,而不是指向构造函数。

4.原型对象的问题

原型模式省去了构造函数传递初始化参数这环节,但是所有的实例化函数也默认的取到了相同的属性。原型中所有属性都是很多实例所共享的,但是对于包含应用类型的属性来说,这就是个问题。例如:

function Person() {};
Person.prototype = {
    constructor: Person, // 把指针指向Person,但是这样做constructor会被枚举,[[ Edition ]] 也会从默认的false变成true
    name: "小明",
    age: 29,
    job: "工人",
    friends: ["one", "two"];
    sayName: function (){
        console.log(this.name)
    }
};

//  修改Person.prototype的constructor属性不可以被枚举
Object.defineProperty(Person.prototype, "constructor", {
    edition: false,
    value: Person
});

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

person1.friends.push("three");
console.log(person1.friends); //  "one", "two", "three"
console.log(person2.friends); //  "one", "two", "three"
console.log(person1.friends === person2.friends); //  true

4)组合模式(组合使用构造函数和原型模式)

组合模式中,构造函数用于定义实例属性,原型模式用于定义方法和共享属性。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["one", "two"];
};
Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log(this.name);
    }
};

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

person1.friends.push("three");
console.log(person1.friends); //  "one", "two", "three"
console.log(person2.friends); //  "one", "two"
console.log(person1.friends === person2.friends); //  false
console.log(person1.sayName === person2.sayName); //  true

5)动态原型模式

function Person(name, age, job){
    //属性  
    this.name = name;
    this.age = age;
    this.job = job;
    //  方法
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name)
        }
    }
};
var person = new Person("小明", 29, "工人");
person.sayName();

使用动态原型模式的时候,不能使用对象字面量重写原型

6)寄生构造函数模式

关于寄生构造函数模式需要说明: 返回的对象与构造函数或者与构造函数的原型属性之间没有什么关系;也就是说构造函数返回的对象和在外部创建的对象没有什么不同。因此也不能使用instanceof来确定对象类型。所以不建议使用这种方式创建模型。

function Person(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function (){
        console.log(this.name);
    }
    return o;
}
var friend = new Person("小明", 29, "老师");
friend.sayName(); // "小明"

7)稳妥构造函数模式

所谓稳妥对象,指没有公共属性,而且其他方法也不引用this的对象。

稳妥构造函数与寄生构造函数类似,但是有两点不同:

  • 1.创建对象的实例方法不引用this;
  • 2.不使用new操作符构造函数。
function Person(name, age, job){
   //  创建要返回的对象
    var o = new Object();
    //  可以定义私有变量和方法
    
    //  添加方法
    o.sayName = function (){
        console.log(name);
    }
    return o;
}
var friend = Person("小明", 29, "老师");
friend.sayName(); // "小明"

在这种方法中除了本身暴露的方法之外,没有任何方式能访问到构造函数内部。也不能使用instanceof来确定对象类型

3.继承

ECMAScript无法实现接口继承。只支持实现继承,而且实现继承主要依靠原型链来实现的

1)原型链继承

思路:用父类实例来充当子类实例对象

缺点:

  • 1.共享(包含引用类型值的原型属性会被所有实例共享)
  • 2.不能传参(没有办法在不影响对象实例的情况下给超类型的构造函数传递参数)
function a() {
    this.type = true;
}
a.prototype.getAValue = function () {
    return this.type;
};
function b() {
    this.property = false;
};
//  继承原型
b.prototype = new a();
b.prototype.getBValue = function () {
    return this.property;
};
var instance = new b();
console.log(instance.getAValue()); //  true

上述例子是一个原型链的基本模式,但是还有大概几个点要注意:

1.别忘记默认的原型

所有的引用类型都是默认继承了Object,而这个继承也是通过原型链实现的。而且所有函数的默认原型都是Object的实例

2.确定原型实例的关系

可以通过instanceof操作符、isPrototypeOf()方法等来确定关系。

3.谨慎的去定义方法

通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。

2)借用构造函数

思路:借父类的构造函数来增强子类实例(等于把父类的实例属性复制了一份给子类实例装上了),主要是通过apply()和call()在先创建的对象上执行构造函数。

优点:可传参(可以在子类型的构造函数向超类型的构造函数传递参数)

缺点:不能复用(超类型原型链中定义的方法对子类是不可见的)。

function Super(name){
    //  实例不会被共享
    this.arr = [1,2,3]
    this.name = name;
};
function Sub(){
    //  继承Super,还同时传递了参数
    Super.call(this,"小明");
    this.age = 29;
};
var instance = new Sub();
console.log(instance.name); // 小明
console.log(instance.age); // 29

3)组合继承

思路:把实例函数放在原型对象上,以实现函数复用

优点:无共享问题,可传参,可复用

缺点:父类构造函数会被调用两次,生成两份,而子类实例上的那一份屏蔽了子类原型上的,内存浪费。

function Super(name){
    this.arr = [1,2,3]
    this.name = name;
};
Super.prototype.sayName = function() {
    console.log(this.name);
};
function Sub(name, age){
    //  继承Super,还同时传递了参数
    Super.call(this, name);
    this.age = age;
};
//  继承方法
Sub.prototype = new Super();
//  把继承到的指针(super)修复,指向自己(sub)
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){
    console.log(this.age);
};
var instance1 = new Sub("小明", 29);
instance1.arr.push(4);
console.log(instance1.name); // 小明
console.log(instance1.age); // 29
console.log(instance1.arr); // [1,2,3,4]

var instance2 = new Sub("小花", 20);
console.log(instance2.name); // 小花
console.log(instance2.age); // 20
console.log(instance2.arr); // [1,2,3]

4)原型式继承

// 原型式继承的原理
function object(o) {
    //  创建一个临时的构造函数
    function F(){}
    //  将传入的对象,作为构造函数的原型
    F.prototype = o;
    //  返回临时类型的实例
    return new F();
};

思路:用Object函数得到一个空的新对象,再逐步增强填充实例属性

优点:从已有的对象上衍生新对象,不需要创建自定义类型

缺点:共享问题

Object.create() :ES5中规范了原型式继承 参数1:用作新对象原型的对象 参数2:为新对象定义额外属性的对象(可选)

本质上这个函数对传入的值进行了一次浅拷贝。

var person = {
    name: "Nicholas",
    arr: [1, 2, 3]
};
var ano = Object.create(person, {
    name: {
        value: "小明"
    }
})
// 会添加在原型上
ano.arr.push(4);
console.log(ano.name); // 小明
console.log(person.arr); // [1,2,3,4]

5)寄生式继承

思路:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,返回对象。

优点:不需要创建自定义类型

缺点:不能复用

function object(o) {
    //  创建一个临时的构造函数
    function F() {}
    //  将传入的对象,作为构造函数的原型
    F.prototype = o;
    //  返回临时类型的实例
    return new F();
}
function createAnother(data){
    //  通过调用函数创建一个新对象
    var clone = object(data);
    // 增强这个对象
    clone.sayName = function(){
        console.log(11);
    };
    //  返回这个对象
    return clone;
};
var person = {
    name: "Nicholas",
    arr: [1, 2, 3]
};
var ano = createAnother(person);
ano.sayName(); // 11

6)寄生组合式

思路:切掉了原型对象上多余的那份父类实例属性

function object(o) {
    //  创建一个临时的构造函数
    function F() {}
    //  将传入的对象,作为构造函数的原型
    F.prototype = o;
    //  返回临时类型的实例
    return new F();
}
function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //指定对象
}

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};
function SubType(name, age) {
    SuperType.call(this, name);  // (关键)
    this.age = age;
}
inheritPrototype(SubType, SuperType);  // (关键)
SubType.prototype.sayAge = function() {
    console.log(this.age);
};
var aa = new SubType("小明", 20);
console.log(aa);
aa.sayName(); // 小明