JavaScript深入理解系列:深拷贝与浅拷贝

导读

在平时工作中或者面试中都会接触到对数据的复制,下面来实现一个比较完整的功能,提供全部思路,下面直接进入正文。

定义

回顾一下JavaScript中存在两大数据类型:
  • 基本数据类型:String、Number、Boolean、Null、Undefined、Symbol(ES6 引入了一种新的原始数据类型,表示独一无二的值)

  • 引用数据类型:Object(Array、Function、RegExp、Date...)

浅拷贝.jpg
  • 浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝.png
  • 深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,并且在修改新对象不会影响原对象。

浅拷贝

浅拷贝常见简单用法:

  • Object.assign
  • Array.prototype.slice、Array.prototype.concat()
  • ES6拓展运算符
  • ...

Object.assign

const obj = {
    age: 20,
    array: ['one', 'two'],
    objs: {
        obj1: 'obj1',
        obj2: 'obj2'
    },
    demo: function () {
        console.log('This is demo!')
    }
}
const newObj = Object.assign({}, obj);

Array.prototype.slice

const array = [ 1, 2, 3 ];
const copyArray = array.slice(0);
copyArray[1] = "改变新数组";
console.log(copyArray) // [ 1, '改变新数组', 3 ]
console.log(array) // [ 1, 2, 3 ] 

Array.prototype.concat()

const array = [ 1, 2, 3 ];
const copyArray = array.concat();
copyArray[1] = "改变新数组";
console.log(copyArray) // [ 1, '改变新数组', 3 ]
console.log(array) // [ 1, 2, 3 ] 

ES6拓展运算符

const array = [ 1, 2, 3 ];
const copyArray = [...array];
copyArray[1] = "改变新数组";
console.log(copyArray) // [ 1, '改变新数组', 3 ]
console.log(array) // [ 1, 2, 3 ] 

深拷贝

常见的深拷贝:

  • JSON.parse(JSON.stringify())
  • _.cloneDeep() (lodash库中的方法,lodash是一套工具库,内部封装了很多字符串、数组、对象等常见数据类型的处理函数等等)
  • jQuery.extend() (jQuery中的方法)
  • 循环递归克隆数据

JSON.parse(JSON.stringify())

const obj = {
    age: 20,
    array: [ 'one', 'two' ],
    objs: {
        obj1: 'obj1',
        obj2: 'obj2'
    },
    demo: function() {
        console.log('This is demo!');
    },
    undefinedTest: undefined,
    symbolTest:  Symbol('A')
};
const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);
// {
//     age: 20,
//     array: [ 'one', 'two' ],
//     objs: { obj1: 'obj1', obj2: 'obj2' }
//  }
// 缺点没有把demo方法/undefinedTest/symbolTest复制,忽略undefined、symbol和函数,所以这个克隆对象不安全,不完善

_.cloneDeep()

const _ = require('lodash'); 
const obj1 = {
    a: 1,
    b: { 
        f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false
// 等于false表示新对象和原对象不是同一个内存引用地址

jQuery.extend()

const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
// 等于false表示新对象和原对象不是同一个内存引用地址

循环递归

既然是深度克隆,那就建一个新对象,然后把需要被克隆对象的每一个值都复制给新对象。

简单版本

const obj = {
    test: '我是文字',
    age: 20,
    name: undefined,
    array: [ 'one', 'two', [ 1, 2 ] ],
    objs: {
        obj1: 'obj1',
        obj2: {
            obj2_1: 'obj2_1'
        }
    }
};

function deepClone(data) {
    let cloneData = {};
    for (const key in data) {
        cloneData[key] = data[key];
    }
    return cloneData;
}
const newObj1 = deepClone(obj);
console.log(newObj1);
// {
//   test: '我是文字',
//   age: 20,
//   name: undefined,
//   array: [ 'one', 'two', [ 1, 2 ] ],
//   objs: { obj1: 'obj1', obj2: { obj2_1: 'obj2_1' } }
// }

简单版本版本就完成了。表面上可以打印新对象跟原来的对象一样了。但是我们上述说了,深度克隆取决于新对象和原来的对象是否共用同一个内存的地址。所以我改一下原对象某一个值,是否影响了新对象。

const obj = {
    test: '我是文字',
    age: 20,
    name: undefined,
    array: [ 'one', 'two', [ 1, 2 ] ],
    objs: {
        obj1: 'obj1',
        obj2: {
            obj2_1: 'obj2_1'
        }
    }
};

function deepClone(data) {
    let cloneData = {};
    for (const key in data) {
        cloneData[key] = data[key];
    }
    return cloneData;
}
const newObj1 = deepClone(obj);
console.log(newObj1);
// {
//   test: '我是文字',
//   age: 20,
//   name: undefined,
//   array: [ 'one', 'two', [ 1, 2 ] ],
//   objs: { obj1: 'obj1', obj2: { obj2_1: 'obj2_1' } }
// }
obj.age = 18;
obj.objs.obj2 = null; // 改原对象某一个值,看看是否影响新对象。
console.log(newObj1)
// {
//   test: '我是文字',
//   age: 20,
//   name: undefined,
//   array: [ 'one', 'two', [ 1, 2 ] ],
//   objs: { obj1: 'obj1', obj2: null } // obj2: null 数据被改变了
// }

上述我们也看到了,修改了原对象数据【obj.age和obj.objs.obj2】,其中只有obj.objs.obj2数据变了,影响了新对象的数据。表示obj.objs.obj2这个数据,新对象和原对象在内存中还是共用了同一个地址。对于这种对象中还有多层数据的,我们使用递归遍历克隆数据到新对象。总结一下:

  • 基本数据类型,不用继续拷贝,直接返回
  • 引用数据类型,创建新对象,遍历需要克隆的数据添加到新对象,如果有多层对象的,并将需要克隆的对象继续深度克隆后添加给新对象,这里就使用递归实现。

那就继续完善一下代码

const obj = {
    test: '我是文字',
    age: 20,
    name: undefined,
    array: [ 'one', 'two', [ 1, 2 ] ],
    objs: {
        obj2: {
            obj2_1: 'obj2_1'
        }
    }
};
function deepClone(data) {
    // 非对象直接返回对应的值(可能是普通的值、函数、正则等等)
    if (typeof data !== 'object') return data;
    let cloneData = {};
    for (const key in data) {
        cloneData[key] = deepClone(data[key]);
    }
    return cloneData;
}
const newObj1 = deepClone(obj);
console.log(newObj1);
// {
//     test: '我是文字',
//     age: 20,
//     name: undefined,
//     array: { '0': 'one', '1': 'two', '2': { '0': 1, '1': 2 } },
//     objs: { obj2: { obj2_1: 'obj2_1' } }
//  }
obj.age = 18;
obj.objs.obj2 = null;
console.log(newObj1);
// {
//     test: '我是文字',
//     age: 20,
//     name: undefined,
//     array: { '0': 'one', '1': 'two', '2': { '0': 1, '1': 2 } },
//     objs: { obj2: { obj2_1: 'obj2_1' } }
//   }

原对象改动了数据【obj.objs.obj2】,新对象 newObj1 不受影响。说明方法对象之间没有共用同一个内存地址,但是方法还有很多不完善,例如数组还没完善,那么我们下面继续完善。

兼容数组

上述方法只考虑了数据是object的情况下,在js中数组用typeof方法来判断数组,也是显示object,那么我们继续来完善一下方法:

function deepClone(data) {
    // 非对象直接返回对应的值(可能是普通的值、函数、正则等等)
    if (typeof data !== 'object') return data;
    // Object.prototype.toString.call(data) 和 Array.isArray 都可以用来判断数据是否数组
    // let cloneData = Array.isArray(data) === '[object Array]' ? [] : {};
    let cloneData = Object.prototype.toString.call(data) === '[object Array]' ? [] : {};
    for (const key in data) {
        cloneData[key] = deepClone(data[key]);
    }
    return cloneData;
}

循环引用

我们来看一个例子:

const obj = {
    test: '我是文字',
    age: 20,
    name: undefined,
    array: [ 'one', 'two', [ 1, 2 ] ],
    objs: {
        obj2: {
            obj2_1: 'obj2_1'
        }
    }
};
obj.objs = obj;
// 我们把方法改一下这样引用。然后执行方法
const newObj1 = deepClone(obj);
console.log(newObj1);
// 控制台看到
// RangeError: Maximum call stack size exceeded // 代表堆栈溢出

运行一下可以看到下图:

error.png

一般看到这种情况是堆栈溢出,原因是对象属性引用了自身的地址,循环引用了。由于直接和间接引用了自身,在克隆对象的时候,就不断的循环创建一块内存地址来存放数据,导致堆栈溢出。

解决这种情况,其实我们可以首先创建一个公共的内存空间来放这个数据,存储当前的对象和引用对象之间的关系,在需要拷贝的时候,首先去存储的公共内存空间找一下,这个公共空间有没拷贝过数据,有的话不用拷贝了,没有的话再拷贝。这样就避免了,一直循环引用,不断创建新的内存空间,导致堆栈溢出。

根据上述描述,这个公共的存储空间,就需要用到 key - value 这种数据结构形式,obj[objData] = objData; 其中objData是一个对象,由于JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。所以我们可以考虑ES6中的 map 数据结构

map数据结构:它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

Map结构有以下2种方法:

  • Map:

    • 1、本质上是键值对的集合,类似集合。
    • 2、可以遍历,方法很多可以跟各种数据格式转换。
  • WeakMap:

    • 1、只接受对象作为键名(null除外),不接受其他类型的值作为键名。
    • 2、键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的。
    • 3、不能遍历,方法有get、set、has、delete。

Map与WeakMap:WeakMap相对Map是弱引用(在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收), 可以看出来WeakMap在性能方面比Map更胜一筹,在浏览器中垃圾回收机制用来清理不会再使用的对象,在某个时刻不用的时候自动被回收了,从而降低内存泄露和数据损坏的可能性。

举个例子:Map

const goodBoy = { 
    name: '程序员米粉', 
    age: 18 
};
const mapObj = new Map();
mapObj.set(goodBoy, '程序员米粉');
goodBoy = null;

上述使用 Map 创建对象,并引用了对象 goodBoy作为键,存在一个强关联关系,虽然把goodBoy置为null,但是mapObj依然对goodBoy还是一个强引用关系,内存还是一直没有释放。

举个例子:WeakMap

const goodBoy = { 
    name: '程序员米粉', 
    age: 18 
};
const mapObj = new WeakMap();
mapObj.set(goodBoy, '程序员米粉');
goodBoy = null;

上述使用 WeakMap 创建对象,并引用了对象 goodBoy作为键,存在弱引用关系,不需要使用的时候,在下一次垃圾回收机制,浏览器会自动回收,内存得到释放。

继续完善一下代码

const obj = {
    test: '我是文字',
    age: 20,
    name: undefined,
    array: [ 'one', 'two', [ 1, 2 ] ],
    objs: {
        obj2: {
            obj2_1: 'obj2_1'
        }
    }
};
obj.objs = obj;
function deepClone(data, map = new WeakMap()) {
    // 非对象直接返回对应的值
    if (typeof data !== 'object') return data;
    // Object.prototype.toString.call(data) 和 Array.isArray 都可以用来判断数据是否数组
    // let cloneData = Array.isArray(data) === '[object Array]' ? [] : {};
    let cloneData = Object.prototype.toString.call(data) === '[object Array]' ? [] : {};
    // 内存公共空间是否有复制过的数据,有就直接返回
    if (map.get(data)) return map.get(data);
    // 存在公共空间对象数据
    map.set(data, cloneData);
    for (const key in data) {
        cloneData[key] = deepClone(data[key], map); // 这里记得存 map
    }
    return cloneData;
}
const newObj1 = deepClone(obj);
结论:假设数据量很大的情况下,在考虑性能方面上,优先选用WeakMap,避免过多的消耗,毕竟这也算JavaScript性能优化的一部分,能优化则优化的原则,让我们写的程序达到最优。

兼容各种数据类型

目前只对普通object和array进行了拷贝。还有其它详细的数据类型没有拷贝,我们详细的列出来,一个一个分别归类进行拷贝。归类的前提需要对数据进行类型判断,我们使用 Object.prototype.toString.call 进行判断。

// Boolean 类型,tag 为 "Boolean"
Object.prototype.toString.call(true);            // => "[object Boolean]"

// Number 类型,tag 为 "Number"
Object.prototype.toString.call(1);               // => "[object Boolean]"

// String 类型,tag 为 "String"
Object.prototype.toString.call("");              // => "[object String]"

// Array 类型,tag 为 "String"
Object.prototype.toString.call([]);              // => "[object Array]"

// Arguments 类型,tag 为 "Arguments"
Object.prototype.toString.call((function() {
return arguments;
})());                                           // => "[object Arguments]"

// Function 类型, tag 为 "Function"
Object.prototype.toString.call(function(){});    // => "[object Function]"

// Error 类型(包含子类型),tag 为 "Error"
Object.prototype.toString.call(new Error());     // => "[object Error]"

// RegExp 类型,tag 为 "RegExp"
Object.prototype.toString.call(/\d+/);           // => "[object RegExp]"

// Date 类型,tag 为 "Date"
Object.prototype.toString.call(new Date());      // => "[object Date]"

// 其他类型,tag 为 "Object"
Object.prototype.toString.call(new class {});    // => "[object Object]

...

// 获取数据类型
function getType(data) {
    return Object.prototype.toString.call(data);
}

那我整理一下标识,这些都是平常用,其实还有更多的,有兴趣的话,自己再查详细点文档,这里不展开

// 可遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

// 不可遍历的类型
const booleanTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
...

上述判断数据类型,出现可遍历属性和不可遍历属性,主要是可遍历属性我们需要用到这些对象原型prototype方法和构造函数constructor,需要遍历这些对象原型上和构造函数上的方法。下面我们就用 constructor这种方式来获取。

function getInit(data) {
    const newCtor = data.constructor;
    return new newCtor();
}

继续改造

const map = new Map();
map.set('flag', '程序员米粉');
const set = new Set();
set.add('程序员米粉');

const obj = {
    test: '我是文字',
    age: 20,
    name: undefined,
    nullFlag: null,
    array: [ 'one', 'two', [ 1, 2 ] ],
    objs: {
        obj2: {
            a: 1
        }
    },
    map,
    set
};
// 可遍历类型
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const mapTag = '[object Map]';
const setTag = '[object Set]';
const argsTag = '[object Arguments]';


// 不可遍历类型
const numberTag = '[object Number]';
const stringTag = '[object String]';
const booleanTag = '[object Boolean]';
const dateTag = '[object Date]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
// ....
// 可遍历类型数据标识
const deeptTagList = [ mapTag, setTag, arrayTag, objectTag, argsTag ];
// 获取数据类型
function getType(data) {
    return Object.prototype.toString.call(data);
}
// 初始类型
function getInit(data) {
    const NewCtor = data.constructor;
    return new NewCtor();
}
// 克隆正则
function deepCloneReg(obj) {
    const data = new obj.constructor(obj.source, /\w*$/.exec(targe));
    data.lastIndex = obj.lastIndex;
    return data;
}
// 其它类型
function otherType(obj, type) {
    const NewCtor = obj.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case symbolTag:
        case dateTag:
            return new NewCtor(obj);
        case regexpTag:
            return deepCloneReg(obj);
        default:
            return null;
    }
}
function deepClone(data, map = new WeakMap()) {
    // 非对象直接返回对应的值
    if (typeof data !== 'object' || !data) return data;
    let cloneData = null;
    // 判断数据类型
    const type = getType(data);
    // 判断是否可遍历类型
    if (deeptTagList.includes(type)) {
        cloneData = getInit(data, type);
    }
    // 克隆map || 克隆set
    if (type === mapTag || type === setTag) {
        data.forEach((val, key) => {
            if (type === mapTag) {
                // 克隆map
                cloneData.set(key, deepClone(val, map));
            } else {
                // 克隆set
                cloneData.add(key, deepClone(val, map));
            }
        });
        return cloneData;
    }
    // Object.prototype.toString.call(data) 和 Array.isArray 都可以用来判断数据是否数组
    // let cloneData = Array.isArray(data) === '[object Array]' ? [] : {};
    cloneData = Object.prototype.toString.call(data) === '[object Array]' ? [] : {};
    // 内存公共空间是否有复制过的数据,有就直接返回
    if (map.get(data)) return map.get(data);
    // 存在公共空间对象数据
    map.set(data, cloneData);
    for (const key in data) {
        cloneData[key] = deepClone(data[key], map); // 这里记得存 map
    }
    return cloneData;
}
const newObj1 = deepClone(obj);
console.log(newObj1);
// {
//     test: '我是文字',
//     age: 20,
//     name: undefined,
//     nullFlag: null,
//     array: [ 'one', 'two', [ 1, 2 ] ],
//     objs: { obj2: { a: 1 } },
//     map: Map(1) { 'flag' => '程序员米粉' },
//     set: Set(1) { '程序员米粉' }
//   }

我们拷贝已经完成了80%了,下面我们继续完成没有完成的类型:String、Number、Boolean、Date、Error

// 其它类型
function otherType(data, type) {
    const NewCtor = data.constructor;
    switch (type) {
        case booleanTag:
            return new Boolean(data);
        case numberTag:
            return new Number(data);
        case stringTag:
            return new String(data);
        case errorTag:
            return new Error(data);
        case symbolTag:
            return new Symbol(data); // 这个我偷懒了,感兴趣可以查一下文档,这里不详细写了
        case dateTag:
            return new NewCtor(data);
        case regexpTag:
            return deepCloneReg(data);
        // 还有函数、等等 可以到loadsh工具库看一下
        default:
            return null;
    }
}
// 克隆正则
function deepCloneReg(obj) {
    const data = new obj.constructor(obj.source, /\w*$/.exec(targe));
    data.lastIndex = obj.lastIndex;
    return data;
}

完整版

const map = new Map();
map.set('flag', '程序员米粉');
const set = new Set();
set.add('程序员米粉');

const obj = {
    name: undefined,
    nullFlag: null,
    array: [ 'one', 'two', [ 1, 2 ] ],
    objs: {
        obj2: {
            a: 1
        }
    },
    map,
    set,
    str: '程序员米粉',
    boolean_: true,
    num: 20,
    symbol: Object(Symbol('程序员米粉')),
    date: new Date(),
    reg: /\d+/,
    error: new Error('错误信息'),
    fn: function(a) {
        return a + b;
    }
};
// 可遍历类型
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const mapTag = '[object Map]';
const setTag = '[object Set]';
const argsTag = '[object Arguments]';

// 不可遍历类型
const numberTag = '[object Number]';
const stringTag = '[object String]';
const booleanTag = '[object Boolean]';
const dateTag = '[object Date]';

const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
// ....
// 可遍历类型数据标识
const deeptTagList = [ mapTag, setTag, arrayTag, objectTag, argsTag ];
// 获取数据类型
function getType(data) {
    return Object.prototype.toString.call(data);
}
// 初始类型
function getInit(data) {
    const NewCtor = data.constructor;
    return new NewCtor();
}

// 其它类型
function otherType(data, type) {
    const NewCtor = data.constructor;
    switch (type) {
        case booleanTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case symbolTag:
            return data;
        case dateTag:
            return new NewCtor(data);
        case regexpTag:
            return deepCloneReg(data);
        // 还有函数、等等 可以到loadsh工具库看一下
        default:
            return null;
    }
}
// 克隆正则
function deepCloneReg(obj) {
    const data = new obj.constructor(obj.source, /\w*$/.exec(obj));
    data.lastIndex = obj.lastIndex;
    return data;
}
function deepClone(data, map = new WeakMap()) {
    // 非对象直接返回对应的值
    if (typeof data !== 'object' || !data) return data;
    let cloneData = null;
    // 判断数据类型
    const type = getType(data);
    // 判断是否可遍历类型
    if (deeptTagList.includes(type)) {
        cloneData = getInit(data, type);
    } else {
        // 不可遍历 
        return otherType(data, type);
    }
    // 克隆map || 克隆set
    if (type === mapTag || type === setTag) {
        data.forEach((val, key) => {
            if (type === mapTag) {
                // 克隆map
                cloneData.set(key, deepClone(val, map));
            } else {
                // 克隆set
                cloneData.add(key, deepClone(val, map));
            }
        });
        return cloneData;
    }
    // Object.prototype.toString.call(data) 和 Array.isArray 都可以用来判断数据是否数组
    // let cloneData = Array.isArray(data) === '[object Array]' ? [] : {};
    cloneData = Object.prototype.toString.call(data) === '[object Array]' ? [] : {};
    // 内存公共空间是否有复制过的数据,有就直接返回
    if (map.get(data)) return map.get(data);
    // 存在公共空间对象数据
    map.set(data, cloneData);
    for (const key in data) {
        cloneData[key] = deepClone(data[key], map); // 这里记得存 map
    }
    return cloneData;
}
const newObj1 = deepClone(obj);

最后输出

错误信息.png

基本的全部功能基本实现,有部分更细节的地方,自己可以查阅相关文档或者lodash工具库,例如:克隆Symbol类型、
克隆Function类型、等等,我这边没有添加上。

参考

结语

希望看完这篇文章对你有帮助:

  • 理解深浅拷贝背后拷贝的原理
  • 手写一个比较完整的深拷贝方法
  • 理解实践这个功能的思路

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注,后续会输出更好的分享。

欢迎关注公众号:【程序员米粉】
公众号分享开发编程、职场晋升、大厂面试经验

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容