JS学习20(高级技巧)

高级函数

函数本质上是很简单且过程化的,但是由于JS天生的动态的特性,从使用方式上可以很复杂。

安全的类型检测

虽然JS中是有类型检测的,但是由于浏览器实现等它们并不完全可靠。比如typeof在Safari中对正则表达式也返回function。
instanceof在存在多个全局作用域时也会把同种却不同作用域中构造函数的实例识别为不同的实例:

var isArray = value instanceof Array;

这个表达式要是想返回true,value必须是个数组,且必须与Array构造函数在同一个全局作用域中,如果value是另一个全局作用域中定义的数组,那这个表达式返回false。
检测某个对象是原生的还是开发人员自定义的对象时也会有问题。因为浏览器开始原生支持JSON了,而有些开发人员还是在用第三方库来实现JSON,这个库里会有全局的JSON对象,这样想确定JSON对象是不是原生的就麻烦了。
解决这些问题的办法就是使用Object的toString方法,这个方法会返回一个[object NativeConstructorName]格式的字符串。

function isArray(value){
    return Object.prototype.toString.call(value) == "[object Array]";
}
function isFunction(value){
    return Object.prototype.toString.call(value) == "[object Function]";
}
function isRegExp(value){
    return Object.prototype.toString.call(value) == "[object RegExp]";
}

不过要注意的是,对于在IE中任何以COM形式实现的函数,isFunction()都会返回false。
对于JSON是否为原生的问题可以这样:

var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) == "[object JSON]";

作用域安全的构造函数

之前我们说的构造函数是这么使用的:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
}
var person = new Person("Nicholas", 29, "Software Engineer");

在这里,由于使用了new操作符,this被绑定在了新创建的Person对象上,如果不用new操作符直接调用Person(),this就会被绑定到window上,这显然是不行的。

function Person(name, age, job){
    if (this instanceof Person){
        this.name = name;
        this.age = age;
        this.job = job;
    } else {
        return new Person(name, age, job);
    }
}
var person1 = Person("Nicholas", 29, "Software Engineer");
alert(window.name);      //""
alert(person1.name);     //"Nicholas"
var person2 = new Person("Shelby", 34, "Ergonomist");
alert(person2.name);     //"Shelby"

不过在使用了这样作用域安全的构造函数后,如果使用基于构造函数窃取的继承,就会有问题:

function Polygon(sides){
    if (this instanceof Polygon) {
        this.sides = sides;
        this.getArea = function(){
            return 0;
        };
    } else {
        return new Polygon(sides);
    }
}
function Rectangle(width, height){
    //这里调用时,传进去的this是Rectangle类型的,没办法拓展side属性
    Polygon.call(this, 2);
    this.width = width;
    this.height = height;
    this.getArea = function(){
        return this.width * this.height;
    };
}
var rect = new Rectangle(5, 10);
alert(rect.sides);        //undefined

解决方法就是使Rectangle也是Polygon的一个实例就好啦

Rectangle.prototype = new Polygon();
var rect = new Rectangle(5, 10);
alert(rect.sides);        //2

惰性载入函数

由于浏览器差异,大量的判断浏览器能力的函数需要被使用(通常是大量的if),然而这些判断一般其实不必每次都执行,在执行一次后,浏览器的能力就确定了,以后就应该不用在判断了。比如:

function createXHR(){
    if (typeof XMLHttpRequest != "undefined"){
        return new XMLHttpRequest();
    } else if (typeof ActiveXObject != "undefined"){
        if (typeof arguments.callee.activeXString != "string"){
            var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                    "MSXML2.XMLHttp"],
                i,len;
            for (i=0,len=versions.length; i < len; i++){
                try {
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString = versions[i];
                    break;
                } catch (ex){
                }
            }
        }
        return new ActiveXObject(arguments.callee.activeXString);
    } else {
        throw new Error("No XHR object available.");
    }
}

这里的创建XHR对象的函数,每次创建对象时都会判断一次浏览器能力,这是不必要的。
惰性载入有两种方式,第一种就是在函数第一次被调用时,根据不同情况,用不同的新函数把这个函数覆盖掉,以后调用就不需要再判断而是直接执行该执行的操作。

function createXHR(){
    if (typeof XMLHttpRequest != "undefined"){
        createXHR = function(){
            return new XMLHttpRequest();
        };
    } else if (typeof ActiveXObject != "undefined"){
        createXHR = function(){
            if (typeof arguments.callee.activeXString != "string"){
                var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                        "MSXML2.XMLHttp"],
                        i, len;
                for (i=0,len=versions.length; i < len; i++){
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                        break;
                    } catch (ex){
                        //skip
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        };
    } else {
        createXHR = function(){
            throw new Error("No XHR object available.");
        };
    }
    return createXHR();
}
createXHR();
alert(createXHR);

第二种思路一样,只不过是在声明函数时就指定新函数,不在第一次调用时再指定。两种办法其实本质上是一样的,看你想怎么用了。

var createXHR = (function(){
    if (typeof XMLHttpRequest != "undefined"){
        return function(){
            return new XMLHttpRequest();
        };
    } else if (typeof ActiveXObject != "undefined"){
        return function(){
            if (typeof arguments.callee.activeXString != "string"){
                var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                    "MSXML2.XMLHttp"],
                    i, len;
                for (i=0,len=versions.length; i < len; i++){
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                        break;
                    } catch (ex){
                        //skip
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        };
    } else {
        return function(){
            throw new Error("No XHR object available.");
        };
    }
})();
alert(createXHR);

函数绑定

函数绑定解决的问题是调用函数时函数的this对象被改为我们不想要的对象的问题。这种情况经常出现在指定事件处理函数时。看个例子:

var handler = {
    message: "Event handled",
    handleClick: function(event){
        alert(this);
        alert(this.message);
    }
};
var btn = document.getElementById("myButton");
EventUtil.addHandler(btn, "click", handler.handleClick);  //[object HTMLButtonElement]  undefined
handler.handleClick();  //   [object Object]   Event handled

这里的this被改成了按钮元素。
解决办法就是将真正的函数套在一个闭包中,以此来保存着个函数的环境

var handler = {
    message: "Event handled",
    handleClick: function(event){
        alert(this); //   [object Object]   
        alert(this.message); //   Event handled
    } };
var btn = document.getElementById("myButton");
EventUtil.addHandler(btn, "click", function(event){
    alert(this);  //[object HTMLButtonElement]
    handler.handleClick(event);
});

可以看到,闭包的this被改为了button,而因为这个闭包的保护,我们的处理函数的环境保存住了。
为了不每次都手动创建一个闭包,我们可以创建一个工具函数bind:

function bind(fn, context){
    alert(arguments[0]);  //fn本身
    return function(){
        alert(arguments[0]); //event
        return fn.apply(context, arguments);
    };
}

这里就是把一个函数使用apply在特定的环境下调用,注意一下arguments对象,最后应该使用的是return过去的匿名函数(闭包)的arguments才对,这样事件给事件处理函数传递的参数才能穿到我们的函数里。

var handler = {
    message: "Event handled",
    handleClick: function(event){
        alert(this); //   [object Object]
        alert(this.message); //   Event handled
    } };
var btn = document.getElementById("myButton");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler));

ES5中为所有函数都定义了一个原生的bind()方法。直接使用就行。

EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler));

只要是将某个函数指针以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效果就显现了

函数柯里化

这个是用来创建已经设置好一个或多个参数的函数,用一个例子看看基本思想:

function add(num1, num2){
    return num1 + num2;
}
function curriedAdd(num2){
    return add(5, num2);
}
alert(add(2, 3));     //5
alert(curriedAdd(3)); //8

创建柯里化函数的通用方式:

function curry(fn){
    var args = Array.prototype.slice.call(arguments, 1);
    return function(){
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);
    };
}

这个函数主要的工作就是将外部函数和内部函数的参数都获取到传递给了返回的函数中。

function add(a,b) {
    alert(a);
    alert(b);
    alert(a+b);
}
var curriedAdd = curry(add, 5);
curriedAdd(3);   //8
curriedAdd = curry(add, 3);
curriedAdd(5);   //8
curriedAdd = curry(add, 5,3);
curriedAdd();   //8

可以将其利用在bind函数中来给事件处理函数传入多个参数。

function bind(fn, context){
    var args = Array.prototype.slice.call(arguments, 2); 
    return function(){
        var innerArgs = Array.prototype.slice.call(arguments); 
        var finalArgs = args.concat(innerArgs);
        return fn.apply(context, finalArgs);
    };
}
var handler = {
    message: "Event handled",
    handleClick: function(name, event){
        alert(this.message + ":"+ name + ":"+ event.type);
    }
};
var btn = document.getElementById("myButton");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "myButton"));

这里要注意的是参数的顺序,比如这里name和event反了可就不对了呦。
ES5中的bind也实现了柯里化,直接使用就可以。

EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler, "myButton"));

防篡改对象

JS共享的本质使任意对象都可被随意修改。这样有时很不方便。ES5增加了几个方法来设置对象的行为。一旦将对象设置为防篡改就不能撤销了。

不可拓展对象

不可以添加新的属性和方法

var person = { name: "Nicholas" };
Object.preventExtensions(person);
person.age = 29;
alert(person.age); //undefined
alert(Object.isExtensible(person)); //false
person.name = "hahah";
alert(person.name); //hahah

密封的对象

不可以添加或删除属性,已有成员的[[Configurable]]被设置为false。

var person = { name: "Nicholas" };
Object.seal(person);
person.age = 29; 
alert(person.age); //undefined
delete person.name; 
alert(person.name); //"Nicholas"
alert(Object.isExtensible(person)); //false
alert(Object.isSealed(person));     //true

冻结的对象

不可拓展,密封,且对象数据属性的[[Writable]]将被设为false。如果定义了[[Set]],访问器属性依然可写。

var person = { name: "Nicholas" };
Object.freeze(person);
person.age = 29; alert(person.age); //undefined
delete person.name; alert(person.name); //"Nicholas"
person.name = "Greg"; alert(person.name); //"Nicholas"

alert(Object.isExtensible(person));//false
alert(Object.isSealed(person));//true
alert(Object.isFrozen(person));//true

高级定时器

setTimeout()和setInterval()是很实用的功能,不过有些事情是要注意的。
JS是单线程的,这就意味着定时器实际上是很有可能被阻塞的。我们在这两个函数中所设置的定时,其实是代表将代码加入到执行队列的事件,如果在加入时恰巧JS是空闲的,那么这段代码会立即被执行,也就是说这个定时被准时的执行了。相反,如果这时JS并不空闲或队列中还有别的优先级更高的代码,那就意味着你的定时器会被延时执行。

重复的定时器

使用setInterval创建定时器的目的是使代码规则的插入到队列中。这个方式的问题在于,存在这样一种可能,在上次代码还没执行完的时候代码再次被添加到队列。JS引擎会解决这个问题,在将代码添加到队列时会检查队列中有没有代码实例,如果有就不添加,这确保了定时器代码被加入队列中的最小间隔是规定间隔。但是在某些特殊情况下还是会出现两个问题,某些间隔因为JS的处理被跳过,代码之间的间隔比预期的小。
所以尽量使用setTimeout()模拟间隔调用。

setTimeout(function(){ 
    setTimeout(arguments.callee, interval);
}, interval);

Yielding Processes

如果你的页面中要进行大量的循环处理,每次循环会消耗大量的时间,那就会阻塞用户的操作。这时分块处理数据就是个好办法。
这个例子每100ms取一个数组元素并添加到页面。

function chunk(array, process, context){
    setTimeout(function(){
        var item = array.shift();
        process.call(context, item);
        if (array.length > 0){
            setTimeout(arguments.callee, 100);
        }
    }, 100);
}
var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342];
function printValue(item){
    var div = document.getElementById("myDiv");
    div.innerHTML += item + "<br>";
}
chunk(data, printValue);

函数节流

这个是为了避免某个操作连续不停的触发,比如涉及到DOM操作,连续大量的DOM操作非常耗资源。
函数节流的基本思想是,每一次调用其实是设置一个真正调用的setTimeout操作,每次调用都会先清除当前的setTimeout再设置一个新的。如果短时间内大量调用,就回一直设置新的setTimeout而不执行setTimeout内的操作。只有停止调用足够长的时间,直到setTimeout时间到了,内部的真正操作才会执行一次。这个对onresize事件特别有用。

function throttle(method, context) {
    clearTimeout(method.tId);
    method.tId= setTimeout(function(){
        method.call(context);
    }, 100);
}
function reDiv(){
    var div = document.getElementById("myDiv");
    div.innerHTML += "qqqqqq" + "<br>";
}
window.onresize = function(){
    throttle(reDiv);
};

这样只有当你停下调整窗口大小100ms后才会执行reDiv操作。

自定义事件

事件这样的交互其实就是观察者模式,这类模式由两类对象组成:主体和观察者。主体负责发布事件,观察者通过订阅这些事件来观察该主体。
创建自定义事件实际上就是创建一个管理事件的对象,并在里面存入各种事件类型的处理函数,触发事件时,只要你给出事件类型,这个对象就会找到相应的事件处理程序并执行。
下面是一个事件管理对象的大体形式:

function EventTarget(){
    this.handlers = {};
}
EventTarget.prototype = {
    constructor: EventTarget,
    addHandler: function(type, handler){
        if (typeof this.handlers[type] == "undefined"){
            this.handlers[type] = [];
        }
    this.handlers[type].push(handler);
    },
    fire: function(event){
        if (!event.target){
            event.target = this;
        }
        if (this.handlers[event.type] instanceof Array){
            var handlers = this.handlers[event.type];
            for (var i=0, len=handlers.length; i < len; i++){
                handlers[i](event);
            }
        }
    },
    removeHandler: function(type, handler){
        if (this.handlers[type] instanceof Array){
            var handlers = this.handlers[type];
            for (var i=0, len=handlers.length; i < len; i++){
                if (handlers[i] === handler){
                    break;
                }
            }
        handlers.splice(i, 1);
        }
    }
};

添加事件处理程序时,addHandler会按照事件的类型将处理函数存入handlers属性中对应的数组里(如果还没有则新建)。
触发事件时使用fire,传入一个至少有type属性的对象。
使用时就像这样:

function handleMessage(event){
    alert("Message received: " + event.message);
}
var target = new EventTarget();
target.addHandler("message", handleMessage);
target.fire({ type: "message", message: "Hello world!"});
target.removeHandler("message", handleMessage);
target.fire({ type: "message", message: "Hello world!"});

自定义事件经常用来解耦对象之间的交互,使用事件就不需要有对象与对象之间的引用,使事件处理和事件触发保持隔离。

拖放

使用原始的鼠标事件

创建一个单例,使用模块模式来创建一个拖动的插件,返回两个方法,分别用来添加和移除所有的事件处理程序。

var DragDrop = function(){
    var dragging = null;
    var diffX = 0;
    var diffY = 0;
    function handleEvent(event){
        event = EventUtil.getEvent(event);
        var target = EventUtil.getTarget(event);
        switch(event.type){
            case "mousedown":
                if (target.className.indexOf("draggable") > -1){
                    dragging = target;
                    diffX = event.clientX - target.offsetLeft;
                    diffY = event.clientY - target.offsetTop;
                }
                break;
            case "mousemove":
                if (dragging !== null){
                    dragging.style.left = (event.clientX - diffX) + "px";
                    dragging.style.top = (event.clientY - diffY) + "px";
                }
                break;
            case "mouseup":
                dragging = null;
                break;
        }
    };
    return {
        enable: function(){
            EventUtil.addHandler(document, "mousedown", handleEvent);
            EventUtil.addHandler(document, "mousemove", handleEvent);
            EventUtil.addHandler(document, "mouseup", handleEvent);
        },
        disable: function(){
            EventUtil.removeHandler(document, "mousedown", handleEvent);
            EventUtil.removeHandler(document, "mousemove", handleEvent);
            EventUtil.removeHandler(document, "mouseup", handleEvent);
        }
    }
}();
DragDrop.enable();

这样看来拖动的功能是实现了,不过有个问题。比如说这是我写的一个插件,使用的人想在拖动开始的时候做一些事情,那么他就不得不在起的源码里做出修改。他需要把所有要执行的代码和函数加到case "mousedown"里。如果这个插件我加密了呢,那想在这个时间点做些事情就更麻烦了。这样的做法显然并不科学。
这时如果使用了自定义事件,就可以很好的解决这个问题。

添加自定义事件

我们在这里新定义一个dragdrop变量,它是EventTarget类型的对象,在它上面我们可以添加事件处理函数或触发事件。在拖动开始时,过程中,结束时,都触发了自定义事件,这样有人想在这几个节点做什么就直接添加事件处理函数就可以了。

var DragDrop = function(){
    //这里的dragdrop是之前的EventTarget类型,可以用来保存和触发事件
    var dragdrop = new EventTarget(),
        dragging = null,
        diffX = 0,
        diffY = 0;
    function handleEvent(event){
        event = EventUtil.getEvent(event);
        var target = EventUtil.getTarget(event);
        switch(event.type){
            case "mousedown":
                if (target.className.indexOf("draggable") > -1){
                    dragging = target;
                    diffX = event.clientX - target.offsetLeft;
                    diffY = event.clientY - target.offsetTop;
                    //触发自定义事件
                    dragdrop.fire({type:"dragstart", target: dragging,
                        x: event.clientX, y: event.clientY});
                }
                break;
            case "mousemove":
                if (dragging !== null){
                    dragging.style.left = (event.clientX - diffX) + "px";
                    dragging.style.top = (event.clientY - diffY) + "px";
                    dragdrop.fire({type:"drag", target: dragging,
                        x: event.clientX, y: event.clientY});
                }
                break;
            case "mouseup":
                dragdrop.fire({type:"dragend", target: dragging,
                    x: event.clientX, y: event.clientY});
                dragging = null;
                break;
        }
    };
    dragdrop.enable = function(){
        EventUtil.addHandler(document, "mousedown", handleEvent);
        EventUtil.addHandler(document, "mousemove", handleEvent);
        EventUtil.addHandler(document, "mouseup", handleEvent);
    };
    dragdrop.disable = function(){
        EventUtil.removeHandler(document, "mousedown", handleEvent);
        EventUtil.removeHandler(document, "mousemove", handleEvent);
        EventUtil.removeHandler(document, "mouseup", handleEvent);
    };
    return dragdrop;
}();
DragDrop.addHandler("dragstart", function(event){
    var status = document.getElementById("myDiv");
    status.innerHTML = "Started dragging " + event.target.id;
});
DragDrop.addHandler("drag", function(event){
    var status = document.getElementById("myDiv");
    status.innerHTML += "<br/> Dragged " + event.target.id + " to (" + event.x +
        "," + event.y + ")";
});
DragDrop.addHandler("dragend", function(event){
    var status = document.getElementById("myDiv");
    status.innerHTML += "<br/> Dropped " + event.target.id + " at (" + event.x +
        "," + event.y + ")";
});
DragDrop.enable();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,297评论 0 6
  • 第一部分 准入训练 第1章 进入忍者世界 js开发人员通常使用js库来实现通用和可重用的功能。这些库需要简单易用,...
    如201608阅读 1,330评论 1 2
  • 古典的代表作《拆掉思维里的墙》,想必很多人都看过了。大家看题目多半觉得这是一碗心灵鸡汤,也许我不能否认,但是我要说...
    讲真书画阅读 545评论 0 8
  • 儿子放暑假,我们搬回了二十公里外东边的家。两年前,为了儿子上学方便,一直在西边的家住着,西边属于高铁新区,...
    花香001阅读 150评论 0 0