深入理解原型到原型链.md

深入理解原型到原型链

说到原型,肯定离不开对象,在JS中,关于原型对象,javaScript高级程序设计中写道:

我们创建的每一个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

上述 图述的代码如下所示,创建一个构造函数,为此构造函数添加prototype属性,person1,person2对象是Person的实例,Person上面的属性和方法共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person() {
}
Person.prototype.name = 'Nicholas';
Person.prototype.age=29;
Person.prototype.job="Software Engineer";
Person.prototype.syaName=function(){
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();

console.log(person1.name===person2.name) // true
//此时执行两次搜索
person1.sysName(); //"Nicholas"

prototype

函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。

__proto__

这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。

为了证明这一点,我们测试:

1
2
3
4
5
function Person() {

}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true

于是我们更新下关系图:

既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

constructor

指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。

为了验证这一点,我们可以尝试:

1
2
3
4
function Person() {

}
console.log(Person === Person.prototype.constructor); // true

此时我们更新关系图:

实例与原型

我们在读取对象属性的时候,要注意搜索顺序,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person() {
}
Person.prototype.name = 'Nicholas';
Person.prototype.age=29;
Person.prototype.job="Software Engineer";
Person.prototype.syaName=function(){
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();

person1.name='Mack';

console.log(person1.name) // Mack
console.log(person2.name) // Nicholas

//此时执行两次搜索
person1.sysName(); //"Mack"

原型的原型

在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:

1
2
3
var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin

其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 __proto__ 指向构造函数的 prototype ,所以我们再更新下关系图:

原型链

在 ECMAScript 中,每个由构造器创建的对象拥有一个指向构造器 prototype 属性值的 隐式引用(implicit reference),这个引用称之为 原型(prototype)。进一步,每个原型可以拥有指向自己原型的 隐式引用(即该原型的原型),如此下去,这就是所谓的 原型链(prototype chain)
可以在浏览器控制台下一直打印自己的原型,返回的都是一样的。

在javaScripts高级程序设计中,关于原型链是这么定义的:

下面我们看一下书上实现原型链的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType() {
this.property = true;
}

SuperType.prototype.getSuperValue = function () {
return this.property;
}

function SubType() {
this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

SuperType.prototype.getSubValue = function () {
return this.subproperty;
};
var instance = new SubType();
console.log(instance.getSuperValue());
console.log(instance.getSubValue());

例子定义了两个类型:SuperType和SubType,每个类型分别有一个属性和一个方法。它们的主要区别是 SubType 继承了 SuperType ,而继承是通过创建 SuperType 的实例,并将该实例赋给SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。在确立了继承关系之后,我们给 SubType.prototype 添加了一个方法,这样就在继承了 SuperType 的属性和方法的基础上又添加了一个新方法。

关系图如下所示:


实际上,所有的引用类型默认都继承了Object,这个继承也是通过原型链实现的。记住,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.peototype。所以完整的关系如下所示:

关于理解js中原型与原型链,其实主要记住这张图就好了,对象的原型属性指向原型对象,原型对象中又有一个属性constructor又指向这个对象,因为这层关系的存在,所以,才会有原型链,进而有继承,正向来说,因为js语言中要设计继承属性,所以才会设计有这么一层关系,都知道js语言设计参考于C++、java等语言,不知道这些语言中关于继承是怎么设计的,但是js的这种设计一开始我看的很迷糊。

后面再会说一下关于对象原型的一个操作方法

原型操作

查看原型

es5带来了查看对象原型的方法——Object.getPrototypeOf,该方法返回指定对象的原型(也就是该对象内部属性[[Prototype]]的值)。

1
2
console.log(Object.getPrototypeOf({}))
//Object.prototype

es6带来了另一种查看对象原型的方法——Object.prototype.__proto__,一个对象的__proto__ 属性和自己的内部属性[[Prototype]]指向一个相同的值 (通常称这个值为原型),原型的值可以是一个对象值也可以是null(比如说Object.prototype.proto的值就是null)。

1
2
({}).__proto__
>>> Object.prototype

创建原型的方式

在下面的例子中我们将对象a的[[Prototype]]指向b。

使用普通语法创建对象

这是最容易被大家忽略的方法,在js中你是绕不过原型的,不经意间就创建了原型

1
2
3
4
5
6
7
8
var o = {a: 1};
// o ---> Object.prototype ---> null

var a = [];
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){}
// f ---> Function.prototype ---> Object.prototype ---> null

这种方法无法让a的[[Prototype]]指向b。

使用构造器创建对象

构造函数就是一个普通的函数,只不过这次不是直接调用函数,而是在函数前加上new关键字。

每个函数都有一个prototype属性,通过new关键字新建的对象的原型会指向构造函数的prototype属性,所以我们可以修改构造函数的prototype属性从而达到操作对象原型的目的。

为了让b继承a,需要有一个构造函数A

1
2
3
4
5
6
7
8
9
10
var b = {};
function A() {};

A.prototype = b;

var a = new A();

Object.getPrototypeOf(a) === b;
// true
// a ---> A.prototype === b

使用 Object.create 创建对象

ES5带来了Object.create接口,可以让我们直接设置一个对象原型

1
2
3
4
5
6
var b = {};
var a = Object.create(b);

Object.getPrototypeOf(a) === b;
// true
// a ---> b

Object.setPrototypeOf

ES6带来了另一个接口,可以绕过创建对象的过程,直接操作原型

1
2
3
4
5
6
7
var a = {};
var b = {};

Object.setPrototypeOf(a, b);
Object.getPrototypeOf(a) === b;
// true
// a ---> b

proto

ES6还带来了一个属性,通过这个属性也可以直接操作原型

1
2
3
4
5
6
7
var a = {};
var b = {};

a.__proto__ = b;
Object.getPrototypeOf(a) === b;
// true
// a ---> b

注意这个属性在ES6规范的附录中,也就意味着不是所有的环境都会有这个属性。

使用 class 关键字

ES6引入了以class语法糖,通过extends关键字我们也可以实现继承,但是无法直接操作对象的原型,而是要借助“类”,其实就是构造函数和函数的prototype属性。

1
2
3
4
5
6
7
8
9
class B {}

class A extends B {}

var a = new A();

Object.getPrototypeOf(a) === A.prototype;
// true
// a ---> A.prototype === B的实例

参考文章:

JavaScript深入之从原型到原型链

详解JavaScript中的原型和继承

全面理解面向对象的 JavaScript

《高程》

Array操作方法.md

Array对象

数组属性

constructor 返回创建数组对象的原型函数。

length设置或返回数组元素的个数。

prototype允许你向数组对象添加属性或方法。

Array 对象属性

改变自身值的方法一共有9个,分别为pop、push、reverse、shift、sort、splice、unshift,以及两个ES6新增的方法copyWithin 和 fill

不会改变自身的方法一共有9个,分别为concat、join、slice、toString、toLocateString、indexOf、lastIndexOf、未标准的toSource以及ES7新增的方法includes。

添加修改删除等操作

浅复制 是指当对象的被复制时,只是复制了对象的引用,指向的依然是同一个对象
concat() 连接两个或更多的数组,组成一个新的数组并返回。

copyWithin( )从数组的指定位置拷贝元素到数组的另一个指定位置中。

slice( start, end )将数组中一部分元素浅复制存入新的数组对象,并且返回这个数组对象。

fill()使用一个固定值来填充数组。

join()将数组中的所有元素连接成一个字符串,并返回这个字符串。

push() 向数组的末尾添加一个或更多元素,并返回新的长度,改变了原数组

unshift()向数组的开头添加一个或更多元素,并返回新的长度。改变了原数组

shift() 删除数组第一个元素,并返回数组的第一个元素,会改变原数组

splice() 从数组中添加或删除元素。会改变原数组

pop()删除数组的最后一个元素并返回删除后的元素,改变了原数组

替换数组

copyWithin(ES6)用于数组内元素之间的替换,即替换元素和被替换元素均是数组内的元素。改变了原数组
arr.copyWithin(target, start[, end = this.length])

fill(ES6)它同样用于数组元素替换,但与copyWithin略有不同,它主要用于将数组指定区间内的元素替换为某个值。改变了原数组

查询数组

every() 检测数值元素的每个元素是否都符合条件。返回Boolean值。

filter() 检测数值元素,并返回符合条件所有元素的数组。

find() 返回符合传入测试(函数)条件的数组元素。当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。

some()检测数组元素中是否有元素符合指定条件。返回布尔值

findIndex() 返回符合传入测试(函数)条件的数组元素索引。同样,检测到第一个的时候,然后返回,不在执行后面的。

indexOf(item,start)搜索数组中的元素,并返回它所在的位置,如果没有,返回-1,但是不区分NaN。

lastIndexOf()返回一个指定的字符串值最后出现的位置,在一个字符串中的指定位置从后向前搜索。

计算转换

reduce()将数组元素计算为一个值(从左到右)。如遇到字符串,执行字符串拼接。

reduceRight()将数组元素计算为一个值(从右到左)。

toString()把数组转换为字符串,并返回结果。

toLocaleString()把数组使用头LocaleString方法转换为字符串,并返回结果。

valueOf()返回数组对象的原始值。

排序

reverse()反转数组的元素顺序,该方法返回对数组的引用,会改变原数组

sort( )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

语法:`arr.sort([comparefn])`comparefn是可选的,如果省略,数组元素将按照各自转换为字符串的Unicode\(万国码\)位点顺序排序。

##### 遍历方法12个

1、**forEach**

[`forEach(function(currentValue,index,arr),thisArg)`](http://www.runoob.com/jsref/jsref-foreach.html) 数组每个元素都执行一次回调函数。

* value 当前正在被处理的元素的值,index 当前元素的数组索引,array 数组本身
* thisArg 可选,用来当做fn函数内的this对象。

2、**every**

使用传入的函数测试所有元素,只要其中有一个函数返回值为 false,那么该方法的结果为 false;如果全部返回 true,那么该方法的结果才为 true。

3、**some**

some 测试数组元素时,只要有一个函数返回值为 true,则该方法返回 true,若全部返回 false,则该方法返回 false。

4、**filter**

filter() 方法使用传入的函数测试所有元素,并**返回所有通过测试的元素组成的新数组**。它就好比一个过滤器,筛掉不符合条件的元素。

语法:`arr.filter(fn, thisArg)`

5、**map**

[`map()`](#)通过指定函数处理数组的每个元素,并返回处理后的**新数组**。

语法:`arr.map(fn, thisArg)`

6、**reduce**

reduce() 方法接收一个方法作为累加器,数组中的每个值(从左至右) 开始合并,最终为一个值。

语法:`arr.reduce(fn, initialValue)`fn 表示在数组每一项上执行的函数,接受四个参数:
* `previousValue` 上一次调用回调返回的值,或者是提供的初始值
* `value` 数组中当前被处理元素的值
* `index` 当前元素在数组中的索引
* `array` 数组自身

`initialValue` 指定第一次调用 fn 的第一个参数。
当 fn 第一次执行时:

* 如果 initialValue 在调用 reduce 时被提供,那么第一个 previousValue 将等于 initialValue,此时 item 等于数组中的第一个值;
* 如果 initialValue 未被提供,那么 previousVaule 等于数组中的第一个值,item 等于数组中的第二个值。此时如果数组为空,那么将抛出 TypeError。
* 如果数组仅有一个元素,并且没有提供 initialValue,或提供了 initialValue 但数组为空,那么fn不会被执行,数组的唯一值将被返回。

7、**reduceRight**

数组中的每个值(从右至左)开始合并,最终为一个值

8、**entries(ES6)**

返回一个数组迭代器对象,该对象包含数组中每个索引的键值对。

9、**find&findIndex(ES6)**

find() 方法基于ECMAScript 2015(ES6)规范,返回数组中第一个满足条件的元素(如果有的话), 如果没有,则返回undefined。

findIndex() 方法也基于ECMAScript 2015(ES6)规范,它返回数组中第一个满足条件的元素的索引(如果有的话)否则返回-1。

语法:`arr.find(fn, thisArg)`,`arr.findIndex(fn, thisArg)`

10、**keys(ES6)**

返回一个数组索引的迭代器。

11、**values(ES6)**

返回一个数组迭代器对象,该对象包含数组中每个索引的值。

12、**Symbol.iterator(ES6)**

###小结:
1. Array.prototype本身就是一个数组,并且它的长度为0。
2. 所有插入元素的方法, 比如 push、unshift,一律返回数组新的长度;
3. 所有删除元素的方法,比如 pop、shift、splice 一律返回删除的元素,或者返回删除的多个元素组成的数组;
4. 部分遍历方法,比如 forEach、every、some、filter、map、find、findIndex,它们都包含function(value,index,array){} 和 thisArg 这样两个形参。
5. Array.prototype 的所有方法均具有鸭式辨型这种神奇的特性。它们不止可以用来处理数组对象,还可以处理类数组对象。




#### Array.of (构造函数)

`Array.of`用于将参数依次转化为数组中的一项,然后返回这个新数组,而不管这个参数是数字还是其它,它基本上与Array构造器功能一致,唯一的区别就在单个数字参数的处理上.

```js
Array.of(8.0); // [8]
Array(8.0); // [undefined × 8]

Array.from (构造函数)

只要一个对象有迭代器,Array.from就能把它变成一个数组(当然,是返回新的数组,不改变原对象)。
语法:Array.from(arrayLike[, processingFn[, thisArg]])
Array.from拥有3个形参,第一个为类似数组的对象,必选。第二个为加工函数,新生成的数组会经过该函数的加工再返回。第三个为this作用域,表示加工函数执行时this的值。后两个参数都是可选的。
注意:一旦使用加工函数,必须明确指定返回值,否则将隐式返回undefined,最终生成的数组也会变成一个只包含若干个undefined元素的空数组。

Array.isArray()

判定一个对象是数组的五种方法,前四种都不保险,如果将某个对象的对象的__proto__属性为Array.prototype,便导致了该对象继承了Array对象,前四种方法就会判定为true.

1
2
3
4
5
6
7
8
9
10
11
var a = [];
// 1.基于instanceof
a instanceof Array;
// 2.基于constructor
a.constructor === Array;
// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a);
// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype;
// 5.基于Object.prototype.toString
Object.prototype.toString.apply(a) === '[object Array]';

所以严格意义上判定一个对象是否是数组,推荐使用第五种方法,再说到Array.isArray(),实际上就是推荐使用的第五种toString方法。

典型问题:

数组去重

实际上有很多种方法,其实都大同小异,只要熟悉数组操作的API方法,你就可以写出很多种,但是本质都是一样的。优先推荐使用ES6中Set属性不重复的特征去重。

  1. 两层for循环,一层遍历数组,一层循环对比,对相同的元素从数组中删除,或者新建一个res数组,将不同的元素push到新数组中,返回新数组。效率低,当数组比较长时不合适。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let arr = [0, 3, 4, 3, 4, 6, 2, 4];
    for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
    if (arr[i] === arr[j]) {
    arr.splice(j, 1);
    j--; //要将值重新向前递进
    console.log(arr)
    }
    }
    }

    // [0, 2, 3, 4, 6]

时间复杂度:O(n^2)

  1. 使用IndexOf或者includes检查是否重复,其实跟第一种方法比较就是将第二层循环使用了IndexOf这种有遍历接口的API操作,这个操作的本质是不是跟第一种方法一样使用循环遍历,这个就是要看源码了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let arr = [0, 3, 4, 3, 4, 6, 2, 4];
    let res = [];
    for (let i = 0, len = arr.length; i < len; i++) {
    let current= arr[i];
    if(res.indexOf(current)===-1){
    res.push(current)
    }

    //或者使用includes
    if(!res.includes(current)){
    res.push(current)
    }

    }

    // [0, 2, 3, 4, 6]

    时间复杂度:

  1. 先用sort排序,或者已知数组是有序的,后比较相邻两个是否相等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let arr = [0, 3, 4, 3, 4, 6, 2, 4];
    arr.sort();
    let temp;
    let res=[]
    console.log(temp)
    for (let i = 0, len = arr.length; i < len; i++) {
    if (!i || temp !== arr[i]) {
    res.push(arr[i])
    }
    temp=arr[i]
    }
    console.log(res);

    // [0, 2, 3, 4, 6]

时间复杂度:有序数组O(n)

  1. ES6中Set属性

    1
    2
    3
    4
    5
    6
    7
    // 去除数组的重复成员
    [...new Set(array)]

    let arr = [0, 3, 4, 3, 4, 6, 2, 4];
    let res=[...new Set(arr)]

    // [0, 2, 3, 4, 6]

    注意
    1、以上去重只对数组中同一种数据类型进行比较去重,如果有不同的数据类型,要区别对待
    2、向 Set 加入值的时候,不会发生类型转换,Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,在 Set 内部,它认为两个NaN是相等。
    3、indexOf内部使用的精确相等运算符(===),NaN===NaN 的结果是 false
    4、includes内部也是使用的类似精确相等运算符(===),NaN===NaN 的结果是 true

1、单数组操作

2、数组遍历方法

3、多数组操作

README.md

本章节主要总结JavaScript中的一些要点。内容比较凌乱, 主要自己总结以及参考大牛的博文。

目录