Skip to main content

2 posts tagged with "数组"

View All Tags

· 6 min read

Vue2.x 不能检测以下数组的变动:1.直接修改数组长度;2.通过数组下标赋值。第一点主要是收到defineProperty的限制,第二点要究其原因,还是得从源码分析。

直接修改数组长度

Vue2.x 利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,实现对数据变化的双向绑定。但是Object.defineProperty并不能监控数组长度的变化,因为数组对象中的 length 属性的 configurablefalse,不允许访问器对该属性进行操作

关于数据双向绑定,可以参考基于 defineProperty 和 proxy 实现数据双向绑定

通过数组下标赋值

vue 怎么监听数组

Observer,是实现 vue 响应式的核心模块,用于监听对象和依赖追踪

// Observer 监听对象
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
if (Array.isArray(value)) {
// 将重写的数组方法挂载到数组原型对象上
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};

可以看到,observer 遇到数组会走单独的处理逻辑,首先将重写的数组方法挂到数组原型方法上,然后进一步通过observeArray处理数组

// observeArray
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
// 如果items[i]是数组,会新建 Observer 对象,然后继续执行observe, 直到遍历完毕
}
};
// observe
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
// 如果要观测的数据不是一个对象或者是 VNode 实例,则不做监听
return;
}
var ob;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
// 已经添加过observer实例,直接返回
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob;
}

可以看到,vue 并没有对数组的所有元素进行监听,所以通过数组下标修改或增加元素,会发现依赖该数组元素的视图并不会更新

重写数组方法

Vue 2.x 针对数组的部分原型方法做了特殊处理,通过调用这些重写的方法,可以对新增的元素的索引进行监听,确保新增元素是响应式的

var methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];

methodsToPatch.forEach(function (method) {
var original = arrayProto[method];
def(arrayMethods, method, function mutator() {
var args = [],
len = arguments.length;
while (len--) args[len] = arguments[len];

var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) {
ob.observeArray(inserted);
}
// 重新触发依赖更新
ob.dep.notify();
return result;
});
});

通过源码可以看到,在操作数组时,如果使用 push、unshift、splice 添加或修改元素,会重新触发依赖的更新函数,渲染对应视图或对象

Vue.set

除了上述方法,我们还可以通过Vue.set来实现对数组的更新,并渲染对应视图。贴一波 set 函数源码,源码分析可以看下注释

function set (target, key, val) {
......
if (Array.isArray(target) && isValidArrayIndex(key)) { // 处理数组元素
target.length = Math.max(target.length, key)
// 使用重写过的splice更新数组
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
// 是对象原有属性,赋值即可触发更新函数
target[key] = val
return val
}
const ob = target.__ob__
// __ob__指向target的observer实例
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
// 对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性
// 所以会直接返回值
return val
}
// target非响应式对象,不用对属性监听,直接赋值并返回
if (!ob) {
target[key] = val
return val
}
// 给新增的对象属性添加依赖
defineReactive(ob.value, key, val)
// 触发依赖,重新渲染页面
ob.dep.notify()
return val
}

· 10 min read

数组是 js 中常见的对象,关于数组的操作很多,作为一个前端攻城狮,应该要能通过灵活应用其内置属性和方法,来解决一系列数组处理问题

初始化数组

  • 构造函数方法

var arr = new Array(3)

var arr = new Array('a','b','c')

var arr = Array(3)

  • 字面量方法 //无法指定成员个数

var arr = ['a','b','c']

数组检查和转化

检查数组类型

let arr = [];
// 1.ES5的isArray方法
Array.isArray(arr);

// 2.constructor指向对象实例的构造函数
arr.constructor === Array;

// 3.instanceof原理同2
arr instanceof Array;

// 4.利用Object的原型方法toString判断(必须是Object原型方法,array和function均重写了这个方法)
// toString方法返回反映这个对象的字符串
//可以用来区分各种类型,但是无法判断自定义对象类型,此时可用2.3方法判断自定义类型
Object.prototype.toString.call(arr) === "[object Array]";

数组转化问题

数组转 String--join(separator)
// 注意区别使用空格和空值
arr.join(" ");
String 转数组--split(separator)
// 根据separator来分割字符串,每一项作为数组的一个值
str.spilt(",");
对象转数组
let obj = { name: "jacksonzhou", age: 23 };
Object.keys(obj); // [name,age]
Object.values(obj); // ['jacksonzhou',23]
Object.entries(obj); // [[name,'jacksonzhou'],[age,23]]
数组转对象
Object.assign({},[1,2]) // 转成{0:1,1:1}
{...['a','b','c']} // 转成{0:'a',1:'b',3:'c'}
// es10的api,浏览器暂不支持
Object.fromEntries([name,'jacksonzhou'],[age,23])
类数组转化为数组

类数组定义:不具有数组所具有的方法,但具有 length 属性,其他属性值为非负整数;常见的类数组有 arguments 对象和 DOM 方法的返回结果(document.getElementsByTagName())

// 使用Array原型方法slice
Array.prototype.slice.call(arguments)
// from方法将类似数组或可迭代对象创建为数组
Array.from(arguments)
// ... (扩展运算符)将类数组扩展为一个列表??(列表实际上是啥?),再定义为数组。
[...arguments]
// 借助对象+indexOf,思路是遍历数组,将每一个值标记为属性,将出现一次的值加入新数组;
//一旦obj.indexOf(i)不为-1,说明重复了,不将它加入新数组

针对数组某项的操作

判断是否包含值

// indexOf
[1, 2, 3]
.indexOf(2) // 1(其对应索引),不存在则为-1
[
// includes
(1, 2, 3)
].includes(2) // true
[
// find
(1, 2, 3)
].find((i) => {
i === 3;
}) // 3,无值返回undefined
[
// findIndex
(1, 2, 3)
].findIndex((i) => {
i === 3;
}); //2,返回索引,无值返回-1

给每一项设置统一值

[1, 2, 3].fill(5); // [5,5,5]

每一项是否满足

[1, 2, 3].every((i) => {
return i > 2;
}); // false

有一项满足

[1, 2, 3].some((i) => {
return i > 2;
}); // true

计算每一项出现的次数

// 数组遍历+借助对象+属性标记+值计数

过滤数组

[1, 2, 3, 4, 5]
.filter((i) => {
return i > 2;
}) // [3,4,5]
[
// 过滤空值
(1, 2, null, 3, 0, false, 4, undefined, "", 5)
].filter(Boolean)
[
// [1,2,3,4,5],但不过滤空的引用类型,比如{}、[]
("1", "2", "3")
].map(parseInt) ===
// 输出[1, NaN, NaN]
["1", "2", "3"].map((item, index) => parseInt(item, index));
  • 简易实现代码
Array.prototype.filterz = function (fn) {
let arr = this;
let newArr = [];
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
newArr.push(arr[i]);
}
}
return newArr;
}[(1, 2, 3, 4, 5)].filterz((i) => {
return i > 2;
});

重装数组

[1, 2, 3].map((i) => {
return "我是" + i;
});

数组乱序

let arr = [1,2,3,4,5,6]
arr.sort(function(){
return Math.random()-0.5
})

数组去重

Map 对象+过滤器 filter

Map 数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键

function unique(arr) {
const res = new Map();
return arr.filter((a) => !res.has(a) && res.set(a, 1));
}

set(集合)去重

Set 数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值。

Array.from(new Set([1,2,2,3]))
[...new Set([1,2,2,3])]

直接使用数组去重

function unique2(arr) {
let newArr = []
for(let i=0;i<arr.length;i++){
if(newArr.indexOf(arr[i])===-1)newArr.push(arr[i])
}
return newArr
}

对象数组去重

let json1 = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 3, name: "c" },
];

let json2 = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 4, name: "d" },
];

let json = json1.concat(json2); //两个数组对象合并
let newJson = []; //盛放去重后数据的新数组
for (item1 of json) {
//循环json数组对象的内容
let flag = true; //建立标记,判断数据是否重复,true为不重复
for (item2 of newJson) {
//循环新数组的内容
if (item1.id == item2.id) {
//让json数组对象的内容与新数组的内容作比较,相同的话,改变标记为false
flag = false;
}
}

if (flag) {
//判断是否重复
newJson.push(item1); //不重复的放入新数组。 新数组的内容会继续进行上边的循环。
}
}

console.log("newJson", newJson);

扁平化数组

// flat是ES10的api, n表示维度, n为 Infinity 时维度为无限大
[[1,2],3].flat(2) // [1,2,3]
[1,[2,3,[4,[...]]]].flat(Infinity) // [1,2,3,4,...]
// 利用toString()实现
let arr = [1,[2,3,[4,5]]]
arr.toString().split(',').map(item=>{
+item // 强制转换为数字类型
})
// 使用迭代的方式实现
let arr = [1, 2, [3, 4, 5, [6, 7], 8]]
function flat(arr) {
while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr) // 将数组型数值追加,并重复追加,知道没有数组型数据
}
return arr
}
flat(arr)
// 使用递归的方式实现
const flat = arr => arr.reduce((acc, cur) => (Array.isArray(cur) ? [...acc, ...flat(cur)] : [...acc, cur]), [])

排序

// sort--关于排序有太多算法知识点,这里只简单实现升序降序,后续再总结相关算法吧~
[1, 3, 2]
.sort((a, b) => {
a - b;
}) // 升序
[(1, 3, 2)].sort((a, b) => {
b - a;
}); // 降序

求最值

// Math.max/min
Math.max(...[1, 2, 3]); // ...将数组转化为参数列表,供max方法使用
Math.max.apply(this, [1, 2, 3])[
// 利用apply可带数组参数
// === Math.max.call(this,...[1,2,3])

// reduce--累加器
(1, 2, 3, 5)
].reduce((pre, cur) => {
return Math.max(pre, cur);
}, 0);
// pre 指上一次return的值,cur指当前值,0指初始值

reducer 累加器原理

function reduce (arr, fn, initVal) {
for (let i = 0; i < arr.length; i++) {
initVal = fn(initVal, arr[i])
}
return initVal
}
let newArr = reduce(arr, (sum, cur) => {
return sum + cur
}, 5)
console.log(newArr)

求和

// 遍历求和
let sum = 0;
[1, 2, 3].map((i) => {
sum += i;
});
console.log(sum);
// reduce -- 累加器
let sum = [1, 2, 3].reduce((prev, current) => {
return prev + current;
}, 0);
// 0 是初始值, prev 是先前return的值,current是当前值

合并

// concat--不会改变原数组,返回一个数组副本
let newArr = arr.concat([1,2,3])
// push--会改变原数组
arr.push([1,2,3])
// ...
[...[1,2,3],...[4,5]]

其他

  • 请把两个数组 ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']['A', 'B', 'C', 'D'],合并为 ['A1', 'A2', 'A', 'B1', 'B2', 'B', 'C1', 'C2', 'C', 'D1', 'D2', 'D']
// 先给第二个数组的值加个数字,让其能按顺序排序,然后再遍历数组去掉第二个数组的值加上的数字
let a1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']
let a2 = ['A', 'B', 'C', 'D'].map((item) => {
return item + 3
})

let a3 = [...a1, ...a2].sort().map((item) => {
if(item.includes('3')){
return item.split('')[0]
}
return item
})

栈和队列操作

栈操作

  • push()
  • pop()

队操作

  • push()
  • shift() // 区别一下 unshift()

扩展运算符...

  • 复制数组
let arr1 = [1, 2, 3];
let arr2 = arr1; // 仅是复制引用,修改arr2会影响arr1
let arr2 = [...arr1]; // 实际上等同于重新声明赋值
  • 转为函数参数
Math.max(...[-1, 1, 1001, -52]); // 1001