Fork me on GitHub

排序算法(十)--堆排序

堆排序思想:堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 Ο(nlogn)。
算法步骤:

  • 创建一个堆 H[0……n-1];
  • 把堆首(最大值)和堆尾互换;
  • 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  • 重复步骤 2,直到堆的尺寸为 1。
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
// 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
var len
var buildMaxHeap = (arr) => { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}
var heapify = (arr, i) => { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}
var swap = (arr, i, j) => {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
var heapSort = (arr) => {
buildMaxHeap(arr);

for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
var arr1=[8,39,400,500,3,4,20,44,440];
console.log(heapSort(arr1))
// [3,4,8,20,39,44,400,440,500]

注:堆排序对堆知识的要求比较高。但是思想相对简单。

排序算法(九)--桶排序

桶排序的思想:桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

  • 在额外空间充足的情况下,尽量增大桶的数量
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
    元素分布在桶中:
    桶排序--1
    元素在每个桶中排序
    桶排序--2
    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
    //  默认bucketSize = 5,默认分5个桶
    var bucketSort = (arr, bucketSize = 5) => {
    if (!arr.length) return
    let min = arr[0]
    let max = arr[0]
    // 用来装桶的桶
    let list = []
    // 返回的结果
    let result = []
    // 获取数组中的最小值和最大值
    for (let i = 1; i < arr.length - 1; i++) {
    max = arr[i] <= max ? max : arr[i]
    min = arr[i] >= min ? min : arr[i]
    }
    // 桶的数量为bucketCount
    let bucketCount = (max - min)/bucketSize
    for (let a = 0; a < arr.length; a++) {
    // 获取桶的编号
    let index = Math.floor((arr[a] - min)/bucketCount)
    if (list[index]) {
    let k = list[index].length - 1
    // 对桶进行排序
    while (k >= 0 && list[index][k] > arr[a]) {
    // 桶前面的数字放到后面去
    list[index][k + 1] = list[index][k]
    k--
    }
    // 不用排序的,直接加在桶的最后面
    list[index][k+1] = arr[a]
    } else {
    // 没有值则生成桶,并把值放到对应的桶中
    list[index]=[];
    list[index][0]=arr[a]
    }
    }
    let n = 0
    while (n <= bucketSize) {
    if (list[n]) {
    result = result.concat(list[n])
    }
    n++
    }
    return result
    }
    arr = [43, 25, 9, 3, 37, 25, 21, 29, 49]
    console.log(bucketSort(arr))
    // [3, 9, 21, 25, 25, 29, 37, 43, 49]
    注:桶排序是一种思想,实际中并不常用。

排序算法(八)--基数排序

基数排序的思想:基数排序并不是比较排序,是桶排序的一种,基数排序要求,在基数排序之前先必须知道排序前的位数,然后将个位数先排,如果相等放在一个桶里,然后是十位,然后是百位,以此类推。
基数排序

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
var radixSort = (arr, numberLength) => {
let mod = 10
let dev = 1
let counter = []
for (let i = 0;
i < numberLength;
i++, mod *= 10, dev *= 10) {
for (let j = 0; j < arr.length;j++) {
let num = parseInt((arr[j] % mod) / dev)
if (!counter[num]) {
counter[num] = []
}
counter[num].push(arr[j])
}
let pos = 0
for (let m = 0; m < counter.length; m++) {
let value = null
if (counter[m]) {
while (value = counter[m].shift()) {
arr[pos++] = value
}
}
}
}
return arr
}
let arr = [342, 234, 675, 543, 23, 543, 764]
// 3是表示最大数的个数
console.log(radixSort(arr, 3))
// [23, 234, 342, 543, 543, 675, 764]

注:基数排序和计数排序是有异曲同工之妙,他们都是在特定的情况下,才可以的。基数排序是稳定的。

排序算法(七)--计数排序

技数排序的思想:

  • 在数组中找到最大的一个,然后生成一个新的数组,计数数组
  • 将原数组中的项作为计数数组的索引,将原数组中项出现的次数记录在计数数组中
  • 将计数组的索引按出现的次数平铺在原数组中,并返回
    计数排序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    var countSort = (arr, max) => {
    let countArr = new Array(max + 1)
    let sortIndex = 0
    for(let i = 0; i < arr.length;i++) {
    if(!countArr[arr[i]]) {
    countArr[arr[i]] = 0
    }
    countArr[arr[i]]++
    }
    for (let j = 0;j < countArr.length;j++) {
    // 判断countArr中存放的次数是大于0的
    while(countArr[j] > 0) {
    arr[sortIndex++] = j
    // 在countArr中存放的是数量
    // 所以需要没存一次就减少一次
    countArr[j]--
    }
    }
    return arr
    }
    let arr = [3,2,4,12,5,5,3,1,7,3,8,5,9,8,9,1]
    countSort(arr, 12)
    // [1, 1, 2, 3, 3, 3, 4, 5, 5, 5, 7, 8, 8, 9, 9, 12]
    注:
    我们可以看到,计数排序并不是像其他排序一样,通过比较大小,所以,计数排序是适合特定情况下的排序,计数排序要求输入的数据必须是有确定范围的整数。

排序算法(六)--快速排序

快速排序的思想:先在整个数列中找到一个基准(pivot),以这个基准为参照,重新排列整个数列,将其中的项目小于基准的排在基准的一边,将大于基准的排在基准的另一侧。等于基准排在哪边无所谓。递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
快速排序

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
quickSort (arr, l, r) {
// 一个元素不需要排序,直接返回
if (l >= r) return arr
// 找到基准的下标
let partitionIndex = partition(arr, l, r)
// 对基准左侧的数组排序,同时基准向左移动
quickSort(arr, l, partitionIndex - 1)
// 对基准右侧的数组排序,同时基准向右移动
quickSort(arr, partitionIndex + 1, r)
}
partition (arr, l, r) {
// 取每段数组的最右边的元素为基准值
let pivot = arr[r]
let left = l
let right = r - 1
while (left < right) {
while (left <= right && arr[left] <= pivot) {
left++
}
while (left <= right && arr[right] > pivot) {
right--
}
if (left < right) {
this.swap(arr, left, right)
}
}
// 最后剩下的元素做交换
swap(arr, left, r)
// 这里返回的是left,不是right
return left
}
swap (arr, i, j) {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
},
let arr = [7,9,4,2,6,1,5,2,3,8]
quickSort(arr, 0, arr.length - 1)
console.log(arr)
// [1,2,3,4,5,6,7,8,9]

注:快速排序稳定,且效率高,所以使用也比较多。

排序算法(五)--归并排序

归并排序的思想:归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

实现的步骤:

  • 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  • 重复步骤 3 直到某一指针达到序列尾;
  • 将另一序列剩下的所有元素直接复制到合并序列尾。

时间复杂度为:nlogn。

归并排序

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
var mergeSort = (arr) => {
if (arr.length < 2) {
return arr
}
let middle = Math.floor(arr.length/2)
let left = arr.slice(0, middle)
let right = arr.slice(middle)
return merge(mergeSort(left), mergeSort(right))
}
var merge = (left, right) => {
let result = []
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift())
} else {
result.push(right.shift())
}
}
while(left.length) {
result.push(left.shift())
}
while(right.length) {
result.push(right.shift())
}
return result
}
mergeSort([4,3,1,9,6,5,2,7,8])
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

归并排序是应用比较多的一种排序,是稳定的,掌握,掌握,掌握!!!
That’s all!

排序算法(四)--希尔排序

希尔排序的思想:希尔排序又被称作为改进版的插入排序,插入排序是相领俩相的比较,然后互换位置。而那么他们的间隔是1,而希尔排序的间隔要大于1,但是最后要执行一次间隔为1的插入排序,这样效率就比较高。
取最优间隔值:h = 3h + 1 ==> 就是间隔去整个length的1/3是相对比较合理的。
希尔排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var hillSort = (arr) => {
let h = 1
while(h <= arr.length/3) {
h = h * 3 + 1
}
for(let gap = h;gap > 0;gap = (gap -1)/3) {
for (let i = gap;i < arr.length;i++) {
for(let j = i;j > gap - 1; j -= gap) {
if (arr[j] < arr[j-gap]) {
let temp = arr[j]
arr[j] = arr[j-gap]
arr[j-gap] = temp
}
}
}
}
return arr
}
hillSort([4,3,1,9,6,5,2,7,8])
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

希尔排序在实际的工作中应用应该比较少。
That’s all!

排序算法(三)--插入排序

插入排序的思想: 插入排序有点像冒泡排序,但是插入排序是从index=1的位置开始,与前面的项做比较,如果比前面的项小,那么就互换位置,或者是将前面较小的项向后移动到对应的位置。
插入排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var insertSort = (arr) => {
for(let i = 1;i < arr.length;i++) {
for(let j = i;j > 0;j--) {
if (arr[j] < arr[j-1]) {
let temp = arr[j]
arr[j] = arr[j - 1]
arr[j - 1] = temp
}
}
}
return arr
}
insertSort([4,3,1,9,6,5,2,7,8])
// [1,2,3,4,5,6,7,8,9]

注:三种基础排序中,插入排序是相对比较有用的一种,尤其是对于样本比较小,且基本有序的情况下,效率会非常的高。
That’s all!

排序算法(二)--冒泡排序

冒泡排序的思想:将数组中的前一项和后一项依此做比较,如果后一项比前一项小,那么互换位置,以此类推,时间复杂度为n2
冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var bubbleSort = (arr) => {
for(let i = 0;i < arr.length - 1;i++) {
for (let j = i + 1;j < arr.length;j++) {
if (arr[i] > arr[j]) {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
}
}
return arr
}
bubbleSort([4,3,1,9,6,5,2,7,8])
// [1,2,3,4,5,6,7,8,9]

注:冒泡排序效率低,基本不用。
That’s all!

排序算法(一)--选择排序

选择排序的思想:将数组的第一项,与后面的每一项做对比,找到最小的那一个,放到第一位,将剩余项所组成的数组的第一项与后面的每一项做对比,找到最小的项放到第一位,以此类推,时间复杂度为n2
选择排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var selectSort = (arr) => {
for (let i = 0; i < arr.length - 1;i++) {
let minPos = i
for (let j = i + 1; j < arr.length;j++) {
minPos = arr[j] < arr[minPos] ? j : minPos
}
let temp = arr[i]
arr[i] = arr[minPos]
arr[minPos] = temp
}
return arr
}
selectSort([7,3,1,5,2,4,6,9,8])
// [1,2,3,4,5,6,7,8,9]

上面的方法是通过找最小值来实现的。那么,我们既然能找到最小值,同时,我们是不是也可以找到最大值呢?这样以来遍历就可以减少一半了,当然是可以的了,且看下面的代码。

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
var selectSort = (arr) => {
let left = 0
let right = arr.length - 1
while(left < right) {
let min = left
let max = right
for(let i = left;i <= right;i++) {
min = arr[i] < arr[min] ? i : min
max = arr[max] < arr[i] ? i : max
}
if (arr[min] < arr[left]) {
let temp = arr[left]
arr[left] = arr[min]
arr[min] = temp
}
if (arr[max] > arr[right]) {
let temp = arr[right]
arr[right] = arr[max]
arr[max] = temp
}
left++
right--
}
return arr
}
selectSort([7,3,1,5,2,4,6,9,8])
// [1,2,3,4,5,6,7,8,9]

注意:选择排序的是一种不稳定的排序,如果出现俩个相邻的值相等,那么往往前一个值会跑到后一个值后面。

浅拷贝和深拷贝是什么,如何手动实现深拷贝

浅拷贝

对于字符串等简单类型的数据来说,浅拷贝就是赋值运算,而对于对象等引用类型的数据来说浅拷贝是对他们的地址的复制。

深拷贝

深拷贝主要是对引用类型的数据说的,深拷贝的主要目的是重新开辟一个栈,俩个数据结构对应俩个不同的地址,修改其中之一,对另一个完全没有影响。

实现深拷贝的方式

JSON.Stringfy

JSON.Stringfy是实现数据拷贝的一种方式,但是该方法也是有坑的:

  • 当对象中存在NaN,Infinity,-Infinity的时候,将会被转为null;
  • 当对象中存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串;
  • 当对象中存在RegExp、Error对象,则序列化的结果将只得到空对象;
  • 当对象中存在函数,undefined,则序列化的结果会把函数, undefined丢失;

不考虑循环引用

1
2
3
4
5
6
7
8
9
10
var deepClone = (item) => {
if (typeof item !== 'object' || item === null) {
return item
}
let result = Array.isArray(item) ? [] : {}
for (let key in item) {
result[key] = deepClone(item[key])
}
return result
}

有循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let obj = {
a: 1,
b: 2,
c: obj
}
var deepClone = (item) => {
const copy = {}
const set = new Set()
for (let [key, value] of Object.entries(item)) {
if (Object(value) !== value) {
copy[key] = value
} else {
if (set.has(value)) {
copy = {...value}
} else {
set.add(value)
copy[key] = deepClone(value)
}
}
}
return copy
}

That’s all!

JavaScript--关于对象你不知道的哪些事儿

学习Javascript,对象是我们再熟悉不过的一种数据结构了,也是一个永远绕不过的话题,但是总有那么些知识,是我们既陌生又熟悉,所以关于对象你不知道的哪些事儿做个归纳总结,还是很有必要的。

创建对象的俩中方式:

  • 构造形式: var obj = new Object()

  • 文字形式:var obj = {}

二者的区别:文字形式可以添加多个key/value,而构造形式必须逐个添加key/value

数据类型:

  • 基本类型:string, number, boolean, undefined, null
  • 引用类型:除了基本类型,其他都是引用类型。

typeof [‘引用类型’] ===> Object
typeof null ===> Object

注意:这些对象在底层都是表示为二进制,在JavaScript中二进制的前三位都是0,那么就会被认为是Object类型,而null的二进制表示全是0,自然前三位也是0,所以typeof null 的结果是Object

Javascript的内置对象:Number, String, Boolean, Object, Array, Function, Date, RegExp, Error

访问属性的方式:

  • .操作符–>属性访问
  • []操作符–>键访问

区别:. 操作符要求属性名满足标识符的命名规范,而 [“..”] 语法 可以接受任意 UTF-8/Unicode 字符串作为属性名。
所以,可以通过动态的方式为对象添加属性:

1
2
3
4
let name = 'name'
let obj = {}
obj[name + 'Vale'] = '小明'
obj.nameValue = '小明'

在对象中,属性名永远都是字符串。如果你使用 string(字面量)以外的其他值作为属性 名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法。

数组:
数组是特殊的对象。所以数组也支持[]的方式来访问,但是数组期望数组下标是整数。也仍然可以给数组添加属性:

1
2
3
4
var myArray = ['foo', 42, 'bar']
myArray.bar = 'bar'
myArray.length // 3
myArray.baz // 'baz'

可以看到虽然添加了命名属性(无论是通过 . 语法还是 [] 语法),数组的 length 值并未发 生变化。

对象的属性描述符:
自从ES5之后,所有的属性都具备了属性描述符:

1
2
3
4
5
6
7
8
9
10
var myObject = {
a: 1
}
Object.getOwnPropertyDescriptor(myObject, 'a')
{
value: 1,
writable: true, // 可写
configable: true, // 可配置
enumerable: true // 可枚举
}

在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty(..) 来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。

1
2
3
4
5
6
7
8
var myObject = {}
Object.getOwnProperty(myObject, 'a', {
value: 1,
writable: true,
configable: true,
enumerable: true
})
myObject.a // 1
  • writable –> 可写,如果值为false,通过myObject.a = 2对对象属性的修改,将会不起作用,在严格模式下,将会报错。
  • confinable –> 可配置,如果值为false,那么对象就成为不可配置,不管是不是严格模式,尝试修改一个不可配置的属性都会报错,而且,confinable修改成false是单向的,无法撤销。而且如果属性confinable是false,我们可以把writable由true改为false,但是不能false改为true。configurable:false 还会禁止删除这个属性。
  • enumerable –> 可枚举,如果值为false,比如在for…in循环中,那么该属性就是不可见的,但是依旧可以正常访问。

对象的不可变性:

  • 对象常量–> 结合writable: false 和confinable: false就可以创建一个真正憝常量属性(不可修改,重新定义,删除)

  • 禁止扩展–> Object.preventExtensions(myObject)不允许在myObject对象上扩展新属性

  • 密封对象–> 会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)。

  • 冻结对象 –> 会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。

对象的Get和Put:

  • 我们知道访问对象属性的方式,应该是先从对象上找某个属性,如果有,那么就返回其属性值,如果没有要查找的属性,那么就回去其原型链找,这个过程就是其内部定义的get方法执行的操作。但是当找不到属性和属性的值是undefined返回的值都是undefined。
    1
    2
    3
    4
    5
    var myObject = {
    a: undefined
    }
    myObject.a // undefined
    myObject.b // undefined
  • Put操作与Get操作有点不同。
    1. 属性是否是访问描述符(参见3.3.9节)?如果是并且存在setter就调用setter。
    2. 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在
      严格模式下抛出 TypeError 异常。
    3. 如果都不是,将该值设置为属性的值。
      如果对象中不存在这个属性,Put操作会更加复杂。

Getter和Setter:
对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法 应用在整个对象上。getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏 函数,会在设置属性值时调用。通常来说getter和setter是成对出现的。

1
2
3
4
5
6
7
8
9
10
11
var myObject = {
// 给 a 定义一个 getter
get a() {
return this._a_;
},
// 给 a 定义一个 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2; myObject.a; // 4

存在性:
上面说,当访问一个对象的属性的时候会返回的是undefined,那么到底是不错在这个属性呢,还是属性值是undefeated?这是个问题。

  • in –> 可以通过in操作符来检查属性是否会在这个对象上,而且in操作符会检查原型链上的属性。
  • hasOwnProperty(…)–>可以检查属性是否会在这个对象,而且不会检查原型链。
  • Object.prototype.hasOwnProperty. call(myObject,”a”)有的对象可能没有连接到Object.prototype( 通 过 Object. create(null) )创建。那么hasOwnProperty就会失败。

检查可枚举:myObject.propertyIsEnumerable()检查属性是不是存在对象上,且是enumerable:true,不会检查原型链。
Object.keys(..) 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举。并不会检查原型链。

遍历:
for…in…通过遍历对象的key来得到属性值,而且遍历的顺序是不可信的
for…of…循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的 next() 方法来遍历所有返回值。for..of 循环每次调用 myObject 迭代器对象的 next() 方法时,内部的指针都会向前移动并 返回对象属性列表的下一个值.知道返回done:true遍历结束。

JavaScript--call,apply,bind的区别以及如何手动实现他们?

call,apply,bind的区别

我们知道call, apply, bind的最大用处就是用来改变函数中this的指向问题。那么他们有什么区别呢?

  • call和apply的区别
    call和apply的第一个参数都是绑定的对象,只是第二个参数不一样,call的第二个参数是一个序列还的列表,apply的第二个参数是一个数组。并且call和apply会立即执行。
  • call, apply和bind的区别
    bind方法是事先把fn的this改变为我们要想要的结果,并且把对应的参数值准备好,以后要用到了,直接的执行即可,也就是说bind同样可以改变this的指向,但和apply、call不同就是不会马上的执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var name = '李雷'
var age = 3
var obj = {
name: '韩梅梅',
age: 8
fun: function (from, to) {
console.log(this.name + this.age + '岁来自' + from + '去往' + to)
}
}
var db = {
name: 'lily',
age: 7
}

obj.fun.call(db, '北京', '上海') // lily7岁来自北京去往上海
obj.fun.apply(db, ['北京', '上海']) // lily7岁来自北京去往上海
obj.fun.bind(db, '北京','上海')() // lily7岁来自北京去往上海
obj.fun.bind(db, ['北京','上海'])() // lily7岁来自北京,上海去往undefined

call, apply的应用

求最大值和最小值

1
2
3
4
5
var arr = [1,4,2,5,76,3,20]
Math.max.call(null, 1,4,2,5,76,3,20) // 76
Math.max.apply(null, arr) // 76
Math.min.call(null, 1,4,2,5,76,3,20) // 1
Math.min.apply(null, arr) // 1

判断数据的类型

1
2
3
Object.prototype.toString.call(null) // "[Object Null]"
Object.prototype.toString.call(1) // "[Object Number]"
Object.prototype.toString.call({}) // "[Object Object]"

数组的拼接

1
2
3
var arr1 = [1, 2, 3]
var arr2 = [4, 5, 6]
[].push.apply(arr1, arr2)

将伪数组转化为数组

函数内的arguments

1
2
3
4
function fun() {
return Array.prototype.slice.call(arguments);
}
console.log(fun(1,2,3,4,5)); // [1,2,3,4,5]

含有length属性的对象

1
2
3
4
5
6
7
let obj4 = {
0: 1,
1: 'thomas',
2: 13,
length: 3 // 一定要有length属性
};
console.log(Array.prototype.slice.call(obj4)); // [1, "thomas", 13]

实现继承

1
2
3
4
5
6
7
8
9
10
11
function Animal (name) {
this.name = name
this.showName = function () {
console.log(this.name)
}
}
function Dog () {
Animal.call(this)
}
let dog = new Dog('小明')
dog.showName() // 小明

手动实现call,apply, bind

call的实现

思路:

  • 将函数设置为对象的属性
  • 执行该方法
  • 删除该方法

注意:

  • this的参数可能为null或者是undefined,此时,this指向window
  • this的类型可能为基本类型,原生的call会将其转为对象
  • 函数是有返回值的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    Function.prototype._call = function (context) {
    context = context ? Object(context) : window
    context.fn = this
    let arg = [], result
    for(let i = 0;i < arguments.length;i++) {
    arg.push('arguments[' + i + ']')
    }
    result = eval('context.fn(' + arg +')')
    delete context.fn;
    return result
    }

    Function.prototype._call = function (context) {
    context = context ? Object(context) : window
    context.fn = this
    let arg = [...arguments].slice(1)
    let result = context.fn(...arg)
    delete context.fn
    return
    }

    apply的实现

    写过了call,再来实现apply应该不是什么问题,因为apply和call的不同之处只是在于第二个参数,代码如下:
    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
    Function.prototype._apply = function (context, arr) {
    context = context ? Object(context) : window
    context.fn = this
    var result
    if (!arr) {
    result = context.fn()
    } else {
    let arg = []
    for (let i = 1;i < argument.length;i++) {
    arg.push('arguments[' + i + ']')
    }
    result = eval('context.fn(' + arg + ')')
    }
    delete context.fn
    return result
    }

    Function.prototype._apply = function (context, arr) {
    context = context ? Object(context) : window
    context.fn = this
    let arg = [...agruments].slice(1)
    let result
    if (Array.isArray(arr) && arr.length) {
    result = context.fn(...arg)
    } else {
    result = context.fn()
    }
    delete context.fn
    return result
    }

    bind的实现

    bind方法的实现主要有以下四点:
  • bind方法接受参数
  • bind方法返回一个新函数
  • bind方法可以指定this
  • 颗粒化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Function.prototype._bind = function (context) {
    if (typeof this !== 'function') {
    throw new Error('请正确使用bind')
    }
    const _this = this
    var arg = [...arguments].slice(1)
    var obind = function () {}
    var bind = function () {
    var bindArg = [...arguments].slice(1)
    return _this.apply(this instanceof obind ? this : context, arg.concat(bindArg))
    }
    obind.prototype = this.prototype
    bind.prototype = new obind()
    return bind
    }

JavasScript--this绑定的原则可不仅仅是谁调用指向谁

说到this的绑定这绝对是让许多程序员非常头疼的一件事儿,所以经过多年的经验总结出一句被广大程序员认为是标杆的经典:this的指向取决于谁调用,谁调用就取决于谁,否则就指向window.由此可见是函数调用的位置决定了this的指向,这句话看似简单,容易理解,可却并不那么简单,所以还是有必要总结一下this绑定的那些事儿。

this的绑定规则是什么?

(一)默认规则

我们先来看一段代码:

1
2
3
4
5
function bar () {
console.log(this.a)
}
var a = 2
bar() // 2

通过上面的代码,我们可以到变量a是声明在全局的变量,当在调用bar的时候,发现this的指向是window全局变量,这时this指向的是全局变量。bar() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。

注意,这里我们使用的是非严格模式,如果是使用严格模式,那么this将会指向的是undefined,并且在严格模式下,this的指向和函数的调用位置无关。

隐式绑定

我们先来看一段代码:

1
2
3
4
5
6
7
8
function bar () {
console.log(this.a)
}
var obj = {
a: 2,
bar: bar
}
obj.bar() // 2

看这段代码,我们声明了一个函数bar,和一个对象obj,他们俩毫无关系,只不过是对象obj有个属性叫bar引用了函数bar,这样以来,在调用到obj.bar()的时候就会使用obj的上下文来引用函数,因此,可以说函数被调用时 obj 对象“拥 有”或者“包含”它。所以当bar被调用的时候,他的落脚点就是指向了obj,当函数引 用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 bar() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
function bar () {
console.log(this.a)
}

var obj = {
a: 2,
bar: bar
}

var a = 'this is window'
var foo = obj.bar
foo() // this is window

可以看到,与上面的代码基本相同,只是多了foo = obj.bar 这一行foo是obj.bar 的一个引用,那么此时foo就是一个不带任何修饰符的函数,那么此时的this就是指向了全局,因此输出的结果是this is window.使用了默认绑定。

这里需要注意的是,回调函数也同样使用该规则

显式绑定

我们先来描述一个场景,隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

是的,你想的没有错,call和apply,在javascript中的绝大多数函数,以及自己创造的函数都可以通过这俩个函数来实现这一功能。它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定

  • 硬绑定
    来看下面的代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function bar () {
    console.log(this.a)
    }
    function foo () {
    bar.call(obj)
    }
    var obj = {
    a: 1
    }

    foo() // 1
    setTimeout(foo) // 1
    foo.call(window)
    我们可以看到,在foo函数中将bar函数的this和obj对象绑定在一起。无论之后如何调用函数foo,它总会手动在 obj 上调用bar。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。而硬绑定的使用场景就:
    (1)创建一个包裹函数,传入所有的参数并返回接收到的所有值:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function foo (item) {
    console.log(this.a, item)
    return this.a + item
    }
    var obj = {
    a: 2
    }
    var bar = function () {
    foo.apply(obj, arguments)
    }
    var b = bar(3) // 2 3
    console.log(b) // 5
    (2)创建一个 i 可以重复使用的辅助函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function foo(something) {
    console.log( this.a, something );
    return this.a + something;
    }
    // 简单的辅助绑定函数
    function bind(fn, obj) {
    return function() {
    return fn.apply(obj, argements)
    };
    }
    var obj = {
    a:2
    }
    var bar = bind( foo, obj );
    var b = bar( 3 ); // 2 3
    console.log( b ); // 5
    注意,由于硬绑定是非常常用的方式,所以ES5提供了内置方法来实现:Function.prototype.bind(),返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。
  • API调用的‘上下文’
    确保你的回调 函数使用指定的 this。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function foo(el) {
    console.log( el, this.id );
    }
    var obj = {
    id: "awesome"
    };
    // 调用 foo(..) 时把 this 绑定到 obj
    [1, 2, 3].forEach( foo, obj );
    // 1 awesome 2 awesome 3 awesome
    这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定,这样你可以少些一些 代码。

    new 绑定

    new操作符对我们来说,再熟悉不过了,所以这里说的new绑定主要就说,在执行new操作符,调用构造函数的时候,绑定的this。
  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
1
2
3
4
5
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

this绑定优先级的判断

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
    var bar = new foo()
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
    var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
    var bar = foo()

this绑定的例外

万物并非全部都在三界之内,有四种猴子却是在这三届之外,那么this绑定四项规则之外也还是有特例存在的。

被忽略的this

如果把null, undefined传入call,apply, bind作为this绑定的对象,那么这些值是会被忽略的,实际应用的是默认绑定。
一般情况下,通过apply来展开一个数组,并且当作是参数传入到一个函数中,同样,bind对参数进行颗粒化的时候,也会有同样的操作:

1
2
3
4
5
6
function foo (a, b) {
console.log('a:' + a, 'b:' + b)
}
foo.apply(null, [1, 2])
var bar = foo.bind(null, 2)
bar(3) // a:2, b:3

我们可以看到,其实函数并不关心this的指向问题,所以,我们需要传入一个null作为站位符。当然了传入null,也是会有副作用的。如果使用了this,那么此操作就将this绑定到了window.

更安全的this

不知道你还记得吗,又一个这样的方法,Object.create(null),这个方法就是创建了一个空对象,这个空要比’{}’更为纯粹,因为他连prototype都没有。
所以,在call,apply,bind中用var obj = Object.create(null)的obj来做占位符将不会有任何的副作用,那么this就会更加的安全。

间接引用

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
console.log( this.a );
}
var a = 2;
var o = {
a: 3,
foo: foo
};
var p = {
a: 4
};
o.foo(); // 3
(p.foo = o.foo)(); // 2

注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到 undefined,否则 this会被绑定到全局对象。

软绑定

软绑定肯定是相对于硬绑定而言,硬绑定这种方式可以把 this 强制绑定到指定的对象,这导致绑定this的灵活性大大降低。如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相 同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。那么就是我们说的软绑定。

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

if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments );
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
function foo() {
console.log("name: " + this.name);
}
var obj = {
name: "obj"
},
obj2 = {
name: "obj2"
},
obj3 = {
name: "obj3"
};
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑定

与内置的bind方法相似,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把 指定的默认对象 obj 绑定到 this,否则不会修改 this。

箭头函数的this词法

ES6中出现的箭头函数,又是一个奇葩,它对于this的判断对于上面的规则并不是适用。在箭头函数中this的指向问题应该是根据外层(函数或者全局)作用域来决定this。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo () {
return (a) => {
console.log(this.a)
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1)
foo.call(obj2) // 2并不是3

所以,我们可以看到,foo的内部返回的函数中捕获的是foo的this,而且箭头函数绑定的this是无法被修改的。
事实上,箭头函数最被常用的地方在于回调函数中,且看下面的几段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var a = 1
function foo () {
setTimeout(() => {
console.log(this.a)
})
}
foo() // 1

var b = 2
function bar () {
setTimeout(function () {
console.log(this.b)
})
}
bar() // undefined

var c = 3
function fun () {
var self = this
setTimeout(function () {
console.log(self.c)
})
}
fun() // 3

看上面的三段代码,区别就在于setTimeout的回调函数。

  • 第一段回调中如果是箭头函数,那么箭头函数中this的指向取决于外层函数,很明显外层函数的this指向window,所以结果是1
  • 第二段回调函数是一个匿名函数,所以this的只想取决于他自己,所以他的this指向的是函数本身
  • 通过var self = this, this的指向取决与fun函数,所以结果是3,self取代了this的机制

所以,一般情况下,我们都会使用self = this 这样的方式来否定this机制,那么,

  1. 只使用词法作用域并完全抛弃错误this风格的代码;
  2. 完全采用 this 风格,在必要时使用 bind(..),尽量避免使用 self = this 和箭头函数。

(完)

微信系列--微信小程序路由跳转问题

最近在开发微信小程序,因为业务需要,页面跳转比较多,总是会出现连续跳转多次后,不能再跳转,或者是屏幕向右滑动回退不能到想要的页面等问题,这都是因为小程序路由栈管理的问题。在微信的官方文档中有五个方法,我们就来总结一下这五个方法的用法,以及遇到的一些坑:

  • wx.switchTab(Object object):switchTab这个方法比较好理解,它主要是用来tab页面的跳转,并且此时会关闭其他非tab的页面;
  • wx.reLaunch(Object object):可以跳转到任意的页面;
  • wx.navigateTo(Object object):正常的页面跳转,上一个页面会保留;
  • wx.redirectTo(Object object):页面重定向,上一个页面会被关闭,不能返回;
  • wx.navigateBack(Object object):页面回退到上一个页面,关闭当前页面。

下面介绍小程序的栈情况:
小程序的想要管理小程序的页面,小程序是通过本身创立的一个小程序栈来管理的。如图:
小程序页面栈示意图
我们知道,栈是遵循先进先出的规则,所以小程序官方提供了getCurrentPages()的方法来获取小程序的栈结构,第一个元素为首页,最后一个元素为当前页面,下程序规定栈中最多存放5个页面。
小程序页面栈示意图
当页面中的栈大于5之后,使用wx.navigateTo()就不会再跳转页面。我们需要明白的一个重要问题就是,当客户按返回按钮的时候究竟会跳转到那个界面,这是我们分析页面栈变化的的意义。首先我们在页面中调用两次navigateTO,页面栈情况如下。
小程序页面栈示意图
这时显示的界面是pageC ,如果客户在此时返回则会一切正常,回退的第一个界面是pageB,然后是pageA。但是如果在pageC 界面调用 wx.redirectTo({url:’pageD’}) 则情况就会不一样了,我们先看一下跳转到pageD后页面栈的情况如何。
小程序页面栈示意图
根据栈的情况,我们可以分析出。如果使用 wx.redirectTo跳转到pageD页面,然后在回退的时候是不能再次回退到pageC的,而会直接回退到pageB。
通过上面对页面栈的分析,我们可以看到栈的变化是会影响客户回退页面的顺序的,所以根据自己的需要合理的使用不同的跳转方法是非常重要的。如果使用不当就会导致跳转混乱让人摸不清头脑。
下面分析一种调转重复页面的情况。如果我们的pageB页面是一个数据列表页面,比如商品列表,pageC是一个商品的编辑界面,一般我们会通过pageB然后进如pageC对商品进行修改,修改后返回pageB。这是很常见的一个场景,但是如果使用不当机会出现如下情况:
小程序页面栈示意图
如图所示栈中出现了两个相同的pageB界面,这个时候如果用户按退出键就会出现一个页面出现2次的情况,而且有一个界面的数据也是旧的数据。因此为了避免这个问题,我们应该在 PageC 页面避免将 PageB重复压入栈中,所以在pageC页面 使用wx.navigateBack({delta:1}); 进行页面回退。而数据刷新的问题则在页面的onShow函数中进行即可。
小程序页面栈示意图

Tips:

  • navigateTo, redirectTo 只能打开非 tabBar 页面。
  • switchTab 只能打开 tabBar 页面。
  • reLaunch 可以打开任意页面。
  • 页面底部的 tabBar 由页面决定,即只要是定义为 tabBar 的页面,底部都有 tabBar。
  • 调用页面路由带的参数可以在目标页面的onLoad中获取。

更多详情。。。。

  • Copyrights © 2015-2022 Lee
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信