[Расширенная фаза 6-3] Выявлена ​​реализация исходного кода метода прототипа массива

внешний интерфейс JavaScript

введение

Несколько общих методов массива используются в【Расширенная фаза 6-1】В сегодняшней статье, представленной в , в основном рассматривается, как эти методы определены в спецификации ECMA-262, и после прочтения спецификации мы используем JS для моделирования реализации и изучения некоторых базовых знаний с помощью исходного кода. Я надеюсь, что эта статья поможет ты помог.

Array.prototype.map

Полная структураArray.prototype.map(callbackfn[, thisArg]),mapФункция получает два параметра: один — требуемая функция обратного вызова, а другой — значение this при выполнении необязательной функции callbackfn.

mapОсновная функция метода заключается в выполнении каждого элемента в исходном массиве по порядку один раз.callbackfnфункцию и объединить все возвращенные результаты для создания нового массива,mapВозвращаемое значение метода — это новый массив.

Документ спецификации ECMA-262 реализован следующим образом.

Развернуть Посмотреть характеристики
  1. Let O be ? ToObject(this value).
  2. Let len be ? LengthOfArrayLike(O).
  3. If IsCallable(callbackfn) is false, throw a TypeError exception.
  4. If thisArg is present, let T be thisArg; else let T be undefined.
  5. Let A be ? ArraySpeciesCreate(O, len).
  6. Let k be 0.
  7. Repeat, while k < len
    1. Let Pk be ! ToString(k).
    2. Let kPresent be ? HasProperty(O, Pk).
  8. If kPresent is true, then
1. Let kValue be ? Get(O, Pk).
2. Let mappedValue be ? Call(callbackfn, T, « kValue, k, O »).
3. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
  1. Set k to k + 1.
  2. Return A.

Используя JS для моделирования реализации, основная логика выглядит следующим образом:
Array.prototype.map = function(callbackfn, thisArg) {
  // 异常处理
  if (this == null) {
  	throw new TypeError("Cannot read property 'map' of null or undefined");
  }
  // Step 1. 转成数组对象,有 length 属性和 K-V 键值对
  let O = Object(this)
  // Step 2. 无符号右移 0 位,左侧用 0 填充,结果非负
  let len = O.length >>> 0
  // Step 3. callbackfn 不是函数时抛出异常
  if (typeof callbackfn !== 'function') {
    throw new TypeError(callbackfn + ' is not a function')
  }
  // Step 4.
  let T = thisArg
  // Step 5.
  let A = new Array(len)
  // Step 6.
  let k = 0
  // Step 7.
  while(k < len) {
    // Step 7.1、7.2、7.3
    // 检查 O 及其原型链是否包含属性 k
    if (k in O) {
      // Step 7.3.1
      let kValue = O[k]
      // Step 7.3.2 执行 callbackfn 函数
      // 传入 this, 当前元素 element, 索引 index, 原数组对象 O
      let mappedValue = callbackfn.call(T, kValue, k, O)
    	// Step 7.3.3 返回结果赋值给新生成数组
      A[k] = mappedValue
    }
    // Step 7.4
    k++
  }
  // Step 8. 返回新数组
  return A
}

// 代码亲测已通过

После прочтения кода это на самом деле довольно просто, ядро ​​находится вwhileвыполнять в циклеcallbackfnи передать параметры 4. Конкретная логика выполнения функции обратного вызова здесь не рассматривается, просто получите возвращаемый результат и назначьте его новому массиву.

Выполняется, только если O и его цепочка прототипов содержат свойство kcallbackfnфункция, поэтому для пустых элементов разреженного массива или используйтеdeleteиндекс после удаленияне будет называться.

let arr = [1, , 3, , 5]
console.log(0 in arr) // true
delete arr[0]
console.log(0 in arr) // false
console.log(arr) // [empty × 2, 3, empty, 5]
arr.map(ele => {
  console.log(ele) // 3, 5
})

mapне изменяет исходный массив, но он не является абсолютным, если вы находитесь вcallbackfnЕсли исходный массив будет изменен, он все равно изменится. Вот в чем проблема.После модификации это повлияет наmapсобственная реализация?

Ответ - да! Однако следует различать следующие ситуации.

  • Новые элементы исходного массива: потому чтоmapДлина уже определена при первом выполнении, поэтому не влияет
  • Исходный измененный элемент массива: переданcallbackfnЭлементы карты возвращаются к своему текущему значению, поэтому могут быть затронуты
    • Измените элемент перед текущим индексом, без изменений
    • Элементы после изменения текущего индекса затронуты
  • Удалить элементы из исходного массива: доступ к удаленным элементам недоступен, поэтому они могут быть затронуты
    • Удалите элемент перед текущим индексом, к которому уже обращались, чтобы он не был затронут
    • Удалить элементы после текущего затронутого индекса

Просто посмотрите на следующие примеры, вcallbackfnНе меняйте исходный массив, иначе возникнут непредвиденные ситуации.

// 1、原数组新增元素,不受影响
let arr = [1, 2, 3]
let result = arr.map((ele, index, array) => {
  array.push(4);
  return ele * 2
})
console.log(result) 
// 2, 4, 6
// ----------- 完美分割线 -----------


// 2、原数组修改当前索引之前的元素,不受影响
let arr = [1, 2, 3]
let result = arr.map((ele, index, array) => {
  if (index === 1) {
    array[0] = 4
  }
  return ele * 2
})
console.log(result) 
// 2, 4, 6
// ----------- 完美分割线 -----------


// 3、原数组修改当前索引之后的元素,受影响
let arr = [1, 2, 3]
let result = arr.map((ele, index, array) => {
  if (index === 1) {
    array[2] = 4
  }
  return ele * 2
})
console.log(result) 
// 2, 4, 8

Наконец, поговоримthis, в исходнике есть такой абзацcallbackfn.call(T, kValue, k, O)Tто естьthisArgЗначение, если оно не установлено, не определено.

согласно с【Продвинутый этап 3-3】Интерпретация вызова в , когда передается undefined, указывает на Window в нестрогом режиме и на undefined в строгом режиме. Помните, что в настоящее время стрелочные функции нельзя использовать в функциях обратного вызова, потому что стрелочные функции не имеют собственного this .

// 1、传入 thisArg 但使用箭头函数
let name = 'Muyiy'
let obj = {
    name: 'Hello',
    callback: (ele) => {
        return this.name + ele
    }
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback, obj);
console.log(result) 
// ["1", "2", "3"],此时 this 指向 window
// 那为啥不是 "Muyiy1" 这样呢,不急,第 3 步介绍
// ----------- 完美分割线 -----------


// 2、传入 thisArg,使用普通函数
let name = 'Muyiy'
let obj = {
    name: 'Hello',
    callback: function (ele) {
        return this.name + ele
    }
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback, obj);
console.log(result) 
// ["Hello1", "Hello2", "Hello3"],完美
// ----------- 完美分割线 -----------

// 3、不传入 thisArg,name 使用 let 声明
let name = 'Muyiy'
let obj = {
    name: 'Hello',
    callback: function (ele) {
        return this.name + ele
    }
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback);
console.log(result)
// ["1", "2", "3"]
// 为什么呢,因为 let 和 const 声明的变量不会挂载到 window 上
// ----------- 完美分割线 -----------

// 4、不传入 thisArg,name 使用 var 声明
var name = 'Muyiy'
let obj = {
    name: 'Hello',
    callback: function (ele) {
        return this.name + ele
    }
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback);
console.log(result)
// ["Muyiy1", "Muyiy2", "Muyiy3"]
// 看看,改成 var 就好了
// ----------- 完美分割线 -----------

// 5、严格模式
'use strict'
var name = 'Muyiy'
let obj = {
    name: 'Hello',
    callback: function (ele) {
        return this.name + ele
    }
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback);
console.log(result)
// TypeError: Cannot read property 'name' of undefined
// 因为严格模式下 this 指向 undefined

В приведенной выше части практического кода представлены пять случаев, а именно два случая передачи в thisArg, два случая в нестрогом режиме и один случай в строгом режиме. Эта часть знаний была представлена ​​в предыдущих статьях, а здесь в основном обзор. Если вы не знакомы с этими знаниями, вы можете прочитать мойблог

Array.prototype.filter

Полная структураArray.prototype.filter(callbackfn[, thisArg]),а такжеmapэто то же самое.

filterКак следует из названия, его основная функция заключается в фильтрации,callbackfnЕсли результат выполнения истинный, то будет возвращен текущий элемент, а если ложный, то не будет возвращен, все возвращенные элементы объединяются для формирования нового массива и возвращаются. Если ни один элемент не проходит проверку, возвращается пустой массив.

Итак, эта часть исходного кода сравниваетmapДругими словами, еще один приговорcallbackfnВозвращаемое значение.

Документ спецификации ECMA-262 реализован следующим образом.

Развернуть Посмотреть характеристики
  1. Let O be ? ToObject(this value).
  2. Let len be ? LengthOfArrayLike(O).
  3. If IsCallable(callbackfn) is false, throw a TypeError exception.
  4. If thisArg is present, let T be thisArg; else let T be undefined.
  5. Let A be ? ArraySpeciesCreate(O, 0).
  6. Let k be 0.
  7. Let to be 0.
  8. Repeat, while k < len
    1. Let Pk be ! ToString(k).
    2. Let kPresent be ? HasProperty(O, Pk).
    3. If kPresent is true, then
      1. Let kValue be ? Get(O, Pk).
      2. Let selected be ! ToBoolean(? Call(callbackfn, T, « kValue, k, O »)).
      3. If selected is true, then
        1. Perform ? CreateDataPropertyOrThrow(A, ! ToString(to), kValue).
        2. Set to to to + 1.
      4. Set k to k + 1.
  9. Return A.

Используя JS для моделирования реализации, основная логика выглядит следующим образом:
Array.prototype.filter = function(callbackfn, thisArg) {
  // 异常处理
  if (this == null) {
  	throw new TypeError("Cannot read property 'map' of null or undefined");
  }
  if (typeof callbackfn !== 'function') {
    throw new TypeError(callbackfn + ' is not a function')
  }

  let O = Object(this), len = O.length >>> 0,
      T = thisArg, A = new Array(len), k = 0
  // 新增,返回数组的索引
  let to = 0
  
  while(k < len) {
    if (k in O) {
      let kValue = O[k]
      // 新增
      if (callbackfn.call(T, kValue, k, O)) {
        A[to++] = kValue;
      }
    }
    k++
  }
  
  // 新增,修改 length,初始值为 len
  A.length = to;
  return A
}

// 代码亲测已通过

пониматьmapГлядя на эту реализацию, все намного проще.callbackfnвозвращаемое значение, добавить индексto, который в основном избегает использованияkПустой элемент генерируется при возврате и модифицируется перед возвратомlengthстоимость.

Эта часть исходного кода довольно интересна, сюрприз в том, чтоA.length = to, ранее не использовался.

Array.prototype.reduce

reduceЭто можно понимать как «объединение», что означает, что море включает в себя все реки, и все мечи едины.Array.prototype.reduce(callbackfn[, initialValue]), второй параметр здесь уже не thisArg, а начальное значениеinitialValue, начальное значение было введено ранее.

  • если не предусмотреноinitialValue, то первый вызовcallbackфункция,accumulatorиспользуя первый элемент в исходном массиве,currentValueТо есть второй элемент в массиве.
  • если предусмотреноinitialValue,accumulatorбудет использовать это начальное значение,currentValueИспользуйте первый элемент исходного массива.
  • вызывается для пустого массива без начального значенияreduceсообщит об ошибке.

Документ спецификации ECMA-262 реализован следующим образом.

Развернуть Посмотреть характеристики
  1. Let O be ? ToObject(this value).
  2. Let len be ? LengthOfArrayLike(O).
  3. If IsCallable(callbackfn) is false, throw a TypeError exception.
  4. If len is 0 and initialValue is not present, throw a TypeError exception.
  5. Let k be 0.
  6. Let accumulator be undefined.
  7. If initialValue is present, then
    1. Set accumulator to initialValue.
  8. Else,
    1. Let kPresent be false.
    2. Repeat, while kPresent is false and k < len
      1. Let Pk be ! ToString(k).
      2. Set kPresent to ? HasProperty(O, Pk).
      3. If kPresent is true, then
        1. Set accumulator to ? Get(O, Pk).
      4. Set k to k + 1.
    3. If kPresent is false, throw a TypeError exception.
  9. Repeat, while k < len
    1. Let Pk be ! ToString(k).
    2. Let kPresent be ? HasProperty(O, Pk).
    3. If kPresent is true, then
      1. Let kValue be ? Get(O, Pk).
      2. Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, k, O »).
    4. Set k to k + 1.
  10. Return accumulator.

Используя JS для моделирования реализации, основная логика выглядит следующим образом:
Array.prototype.reduce = function(callbackfn, initialValue) {
  // 异常处理
  if (this == null) {
  	throw new TypeError("Cannot read property 'map' of null or undefined");
  }
  if (typeof callbackfn !== 'function') {
    throw new TypeError(callbackfn + ' is not a function')
  }
  let O = Object(this)
  let len = O.length >>> 0
  let k = 0, accumulator
  
  // 新增
  if (initialValue) {
    accumulator = initialValue
  } else {
    // Step 4.
    if (len === 0) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
    // Step 8.
    let kPresent = false
    while(!kPresent && (k < len)) {
      kPresent = k in O
      if (kPresent) {
        accumulator = O[k] 
      }
      k++
    }
  }
  
  while(k < len) {
    if (k in O) {
      let kValue = O[k]
      accumulator = callbackfn.call(undefined, accumulator, kValue, k, O)
    }
    k++
  }
  return accumulator
}

// 代码亲测已通过

Эта часть исходного кода предназначена в основном дляinitialValueОбработка относительно проста, когда есть начальное значение, то естьaccumulator = initialValue ,kValue = O[0].

Без обработки начального значения на шаге 8, когда цикл определяется по наличию атрибутов k цепи прототипа и O,accumulator = O[k] и выйти из цикла, потому чтоk++,такkValue = O[k++].

Дополнительные методы массиваfind,findIndex,forEachд., его реализация в исходном коде тоже аналогична, не более чем вcallbackfn.callСделайте некоторую обработку в этой части.Если вам интересно, вы можете заглянуть на официальный сайт TC39 и MDN.Справочная часть имеет прямую ссылку.

Уведомление

forEachисходный код иmapТо же самое, просто сделайте несколько модификаций на основе исходного кода карты.

Array.prototype.forEach = function(callbackfn, thisArg) {
  // 相同
  ...
  while(k < len) {
    if (k in O) {
      let kValue = O[k]
 
      // 这部分是 map
      // let mappedValue = callbackfn.call(T, kValue, k, O)
      // A[k] = mappedValue
      
      // 这部分是 forEach
      callbackfn.call(T, kValue, k, O)
    }
    k++
  }
  // 返回 undefined
  // return undefined
}

Как видите, разница в том, что никакой обработкиcallbackfnРезультат выполнения не возвращается.

Это указано, потому что я раньше видел неправильное утверждение под названием «forEach будет пропускать пустое, но карта не пропускается».

зачем говоритьmapОн не пропускает, потому что, когда исходный массив имеет пустые элементы, результат, возвращаемый картой, также содержит пустые элементы, поэтому он не пропускает, но это утверждение неверно.

let arr = [1, , 3, , 5]
console.log(arr) // [1, empty, 3, empty, 5]

let result = arr.map(ele => {
  console.log(ele) // 1, 3, 5
  return ele
})
console.log(result) // [1, empty, 3, empty, 5]

СмотретьeleВывод поймёт, что карта тоже гэппинг, причина в том, что в исходном кодеk in O, здесь нужно проверить, содержит ли O и его цепочка прототипов атрибут k, поэтому в некоторых реализациях используетсяhasOwnPropertyтакже неверно.

Кроме тогоcallbackfnВы не можете использовать break, чтобы выйти из цикла, потому что break может только выйти из цикла, иcallbackfnНе тело цикла. Если у вас есть похожие потребности, вы можете использоватьfor..of,for..in,some,everyЖдать.

После ознакомления с исходным кодом многие проблемы будут решены легко, спасибо за чтение.

Ссылаться на