前言
这次我们先巩固知识,再看问题。
刚刚入门的前端程序员,或者其他语言的程序员,一般都会先学习该语言的变量类型。
在前端的javascript中,变量分为值类型(简单类型)与引用类型(复杂类型),以及ES6新出的symbol类型,Set和Map数据结构。
其中值类型有string、number、boolean、undefined、null五种,当它们赋值给变量时,该变量直接存储其值于内存栈中。
引用类型有object,其中function、array本质上也属于object,其特点是,当它们赋值给变量时,变量存储的是它们的指针,也就是存储地址,存于内存栈中。其真实的值存放在了该地址所指向的内存空间中,也就是内存堆中.
道理我们都懂,但仍过不好这一生仍然会踩坑。
举个例子
让我们来看一个简单的:
值类型的变量,由于存储的直接是该变量对应的值,所以变量之间是互不影响的:
var a = 1
var b = a
b += 1
console.log('a', a) // 1
console.log('b', b) // 2
引用类型的变量,由于存储的是对应值得指针,也就是存储地址,所以变量之间会相互影响:
var obj1 = {
c: 1
}
var obj2 = obj1
obj2.c = 2
console.log('obj1', obj1) // {c: 2}
console.log('obj2', obj2) // {c: 2}
神奇的事情发生了!我并没有改变obj1,但是其中的a值居然发生了变化.这是因为obj2和obj1共用一个指针,改变了其中一个,存储于堆中的值就会发生改变,另一个也会相应变化,这就是引用类型的存储方式带来的弊端,如图:
发现了问题,就该解决问题.如何解决这种弊端?
涉及到一个前端面试中常见的概念:深浅拷贝
深浅拷贝
浅拷贝: 很简单,就是上述代码中实现的方式 -- 使用表达式直接赋值.
深拷贝: 将引用类型存储方式带来的弊端消除, 也就是拷贝后的值与拷贝前的值不形成相互影响.大概有这么几种方式:
1. 递归遍历
2. 使用原生API进行编码和解码
3. 使用社区现有组件
1.递归遍历:
像上面的例子,对象的成员是Number类型,只需要对新变量遍历赋值就行.但如果对象成员也是Object类型,成员的成员也是Object类型,就不能单纯遍历这么简单了.这时候就要使用到递归:
var deepClone = function(currobj){
if(typeof currobj !== 'object'){
return currobj;
}
if(currobj instanceof Array){
var newobj = [];
}else{
var newobj = {}
}
for(var key in currobj){
if(typeof currobj[key] !== 'object'){
// 不是引用类型,则复制值
newobj[key] = currobj[key];
}else{
// 引用类型,则递归遍历复制对象
newobj[key] = deepClone(currobj[key])
}
}
return newobj
}
缺陷:这个方法的主要问题就是不处理循环引用,不处理对象原型,函数依然是引用类型。上面描述过的复杂问题依然存在,可以说是最简陋但是日常工作够用的深拷贝方式。
2. 使用原生API进行编码和解码
比较巧妙的方法,使用JSON API进行序列化,先编码再解码:
// 调用JSON内置方法先序列化为字符串再解析还原成对象
newObj = JSON.parse(JSON.stringify(obj))
缺陷:JSON是一种表示结构化数据的格式,只支持简单值、对象和数组三种类型,不支持变量、函数或对象实例。所以我们工作中可以使用它解决常见问题,但也要注意其短板:函数会丢失,原型链会丢失,以及上面说到的所有缺陷。
3.使用社区现有组件
以上两种方式可以解决大部分业务场景,但都有缺陷.本文推荐引入社区现有组件来解决问题,不仅简单易用,还不用担心后遗症.常用的组件有以下两种:
- lodash库(强烈推荐)
lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库,提供了很多处理数据的方法.我们要使用的是它的cloneDeep方法:
import _ from 'lodash'
var obj1 = {
c: 1
}
var obj2 = _.cloneDeep(obj1)
obj2.c = 2
console.log('obj1', obj1) // {c: 1}
console.log('obj2', obj2) // {c: 2}
可以看到,使用lodash后,obj2对obj1进行了深拷贝,二者不会相互干扰,并且对任何复杂结构都有效,没有后顾之忧.
- jQuery库
jQuery这个库,是前端新人必学的,也是很多没有专职前端的公司的后端开发人员使用的库,优点是简化了繁琐的原生JS DOM操作,社区强大.但其本质还是操作DOM,对性能没有优化,大公司已基本弃用,转为使用高性能的流行框架react/vue/angular,无需操作DOM,有高效的渲染机制.
但jQuery的某些方法还是可以参考的,比如我们要介绍的extend方法:
// 进行深度复制,如果第一个参数为true则深度复制,如果目标对象不合法,则抛弃并重构为{}空对象,如果只有一个参数则功能为扩展jQuery对象
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[ 0 ] || {},
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
// 第一个参数可以为true来确定进行深度复制
if ( typeof target === "boolean" ) {
deep = target;
// Skip the boolean and the target
target = arguments[ i ] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
// 如果目标对象不合法,则强行重构为{}空对象,抛弃原有的
if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
target = {};
}
// Extend jQuery itself if only one argument is passed
// 如果只有一个参数,扩展jQuery对象
if ( i === length ) {
target = this;
i--;
}
for ( ; i < length; i++ ) {
// Only deal with non-null/undefined values
// 只处理有值的对象
if ( ( options = arguments[ i ] ) != null ) {
// Extend the base object
for ( name in options ) {
src = target[ name ];
copy = options[ name ];
// Prevent never-ending loop
// 阻止最简单形式的循环引用
// var obj={}, obj2={a:obj}; $.extend(true, obj, obj2); 就会形成复制的对象循环引用obj
if ( target === copy ) {
continue;
}
// 如果为深度复制,则新建[]和{}空数组或空对象,递归本函数进行复制
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
( copyIsArray = Array.isArray( copy ) ) ) ) {
if ( copyIsArray ) {
copyIsArray = false;
clone = src && Array.isArray( src ) ? src : [];
} else {
clone = src && jQuery.isPlainObject( src ) ? src : {};
}
// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy );
// Don't bring in undefined values
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// Return the modified object
return target;
};
react渲染机制的一些坑
用过react的人都知道,react控制数据的机制就是:将数据挂在到组件的state上,如果要使用的话,就从state中取,要存的话,就用setState来设置.
那么,问题来了:
如果存储的数据是引用类型,且同时在两个地方对此数据进行了处理,那么,不管你有没有进行setState,数据都会发生联动变化,导致不合预期的结果.比如,我在父组件拿到数据时加了一个is_selected
字段:
...
let list = res.data || res.result
list.photos = list.photos.map((v) => {
v.is_selected = true
return v
})
this.setState({
selectedRows: [list],
visibleDelPhotos: true
})
...
页面展示是这样的:
当用户进行点击checkbox时,我对该字段进行了改变:
// 选择要删除的违规图片
onDelPhotoChange(data, e) {
const value = e.target.checked
let selectedRows = _.cloneDeep(this.state.selectedRows)
// 改变是否选中状态
selectedRows[0].photos = selectedRows[0].photos.map((v) => {
if (v.url === data.url) v.is_selected = value
return v
})
this.setState({ selectedRows })
},
由于业务需要,子组件对数据进行了筛选,将没有勾选的图片剔除:
componentWillReceiveProps(nextProps) {
let selectedRows = nextProps.selectedRows
...
// 单独删除图片,筛选未勾选的图片并处理数据结构
if (typeof selectedRows[i].photos[0] !== 'string') {
selectedRows[i].photos = selectedRows[i].photos.filter(
(v) => v.is_selected !== false && v.is_dirty
)
selectedRows[i].photos = selectedRows[i].photos.map((v) => v.url)
}
...
}
这时候出现了奇怪的情况: 取消勾选后,重新勾选时,该图片消失了.
当时打印出的数据和逻辑都是正常的, 查了很久都没查出问题, 后来想到, 是否是引用类型的联动变化引起的坑?遂使用lodash进行深拷贝:
componentWillReceiveProps(nextProps) {
let selectedRows = _.cloneDeep(nextProps.selectedRows)
...
}
世界清静了, bug没有了.
反过来推理原因: 父子组件对同一数据源进行操作, 子组件筛选掉了未勾选的图片, 导致父组件展示图片时该条数据消失了.
所以这里建议,:
- 遇到state中存取引用类型数据时, 都用lodash进行取值, 以防万一;
- 碰到类似的问题, 如果逻辑没有漏洞, 那么优先考虑是否有在两个地方操作同一数据源的情况, 是否是引用类型带来的坑.
react用久了, 多少会遇到几次这样的坑, 其他框架也类似.望引以为戒.
更新:
发现一种更简便的,不用引入第三方插件的原生方法:
var copy = Object.assign({}, data)
亲测有效:
var obj1 = { a: 1 };
var obj2 = Object.assign({}, obj1);
obj2.a = 2;
console.log(obj1.a); // 1