十个练习题目,感觉比较典型,分享一下。

#累加函数addNum

实现一个累加函数addNum,参数为number 类型,每次返回的结果= 上一次计算的值+ 传入的值

var addNum = (function() {
    var result = result || 0;
    return function(num) {
        result += num;
        return result;
    };
})();
addNum(10);          // 10
addNum(12);          // 22
addNum(30);          // 52

闭包实现即可。addNum右边为一个立即执行函数,返回了一个函数,此函数在内存中,所以其所依赖的result也还在内存中,不会被回收,从而实现缓存的效果。

灵活的应用闭包,能方便很多问题,再看下面一个例子:

/////////////
// 求斐波那契数列 //
/////////////
var count = 0;

// 直接递归
function fib(n) {
    count++;
    if (n < 0) return 0;
    if (n === 0 || n === 1) return 1;
    // 大于2时递归
    // arguments.callee 返回正在执行的Function对象
    return arguments.callee(n - 1) + arguments.callee(n - 2);
}
console.time('fib(30)');
console.log('fib(30),结果为:', fib(30), ',计算次数:', count); //fib(30),结果为: 1346269 计算次数: 2692537
console.timeEnd('fib(30)'); //fib(30): 115.944ms //本机多次测试100ms以上



// 闭包缓存方式
count = 0;
var fibWithCache = (function() {
    var result = []; // 缓存结果        

    return function(n) {            
        var res = result[n];
        // 存在直接取出,否则递归计算
        if (res != undefined) {
            return res;
        } else {
            if (n < 0) return null;
            if (n === 0 || n === 1) {
                res = 1;
            } else {
                count++;
                res = arguments.callee(n - 1) + arguments.callee(n - 2);
            }
        }
        result[n] = res;
        //console.log(result);
        return result[n];
    };
})();

console.time('fibWithCache(30)');
console.log('fibWithCache(30),结果为:', fibWithCache(30), ',计算次数:', count); 
//fibWithCache(30),结果为: 1346269 计算次数: 29
console.timeEnd('fibWithCache(30)'); 
//fibWithCache(30): 0.312ms //本机多次测试均小于1ms

   //  之后再调用小于30的项目,将会直接取出,不用计算。
count = 0;
console.time('fibWithCache(9)');
console.log('fibWithCache(9),结果为:', fibWithCache(9), ',计算次数:', count);
console.timeEnd('fibWithCache(9)');
// fibWithCache(9),结果为: 55 ,计算次数: 0
// fibWithCache(9): 0.215ms

// 计算更大的 也变得很高效
count = 0;
console.time('fibWithCache(32)');
console.log('fibWithCache(32),结果为:', fibWithCache(32), ',计算次数:', count);
console.timeEnd('fibWithCache(32)');
// fibWithCache(32),结果为: 3524578 ,计算次数: 2
// fibWithCache(32): 0.224ms

使用闭包,函数所依赖的result数组将不会被系统的垃圾回收机制回收,将它用来缓存,使得性能得到大幅得的提升。

关于闭包有以下三个特性:

  • 函数可以引用定义在其外部作用域的变量。
  • 闭包比创建他们的函数具有更长的生命周期。(即使外部函数已经返回,闭包函数仍然可以引用在外部函数中定义的变量,例如上面两个例子中用来缓存上次累加结果的result和斐波拉切数列缓存数列的result数组。)
  • 闭包在内部存储其外部变量的引用,并能读写这些变量。(上两例中,闭包对两个外部函数中的result不仅可读,而且可写。)

#实现一个Person类

实现一个Person 类,有2 个属性name,gender(性别),有一个sayHello 方法.

////////////////////////////////////////////////////////
// 实现一个Person 类,有2 个属性name,gender(性别),有一个sayHello 方法. //
////////////////////////////////////////////////////////
// 构造函数
function Person(name, gender) {
    // 避免忘记使用new命令
    if (!(this instanceof Person)) {
        return new Person(name, gender);
    }
    this.name = name;
    this.gender = gender;
}
Person.prototype.sayHello = function() {
    console.log('Hello,I am', this.name, '. I\'m a', this.gender);
};
var zs = new Person("zs", "man");
console.log(zs);        // Person {name: "zs", gender: "man"}
zs.sayHello();          // Hello,I am zs . I'm a man

注意sayHello方法不是写在构造函数里面,而是写在构造函数的原型上。这是因为如果写在构造函数里,将会为每个实例对象给添加一个自己的sayHello方法,而这是没有必要的,每个实例对象的sayHello方法都一样,写在构造函数的原型上就可以使得每个实例对象都能引用到此方法。

关于构造函数的new命令,原理是这样的:

  1. 创建一个空对象,作为将要返回的对象实例
  2. 将这个空对象的原型,指向构造函数的prototype属性
  3. 将这个空对象赋值给函数内部的this关键字
  4. 开始执行构造函数内部的代码

更多关于原型和构造函数的具体知识请访问:面向对象编程概述

#基于Person 类,增加一个static 方法getNum(), 返回创建的实例数

为了实现计数功能,只需要在每次调用构造函数的时候,递增1即可,构造函数已经存在,不能修改,所以直接重写一遍

function Person(name, gender) {
    // 避免忘记使用new命令
    if (!(this instanceof Person)) {
        return new Person(name, gender);
    }
    this.name = name;
    this.gender = gender;
    Person._count += 1;
}
Person.getNum = (function() {
    Person._count = 0;    
    return function() {
        return Person._count;
    };
})();

var p1 = new Person('aaa', 'male');
var p2 = new Person('bbb', 'female');
Person.getNum(); // 2
var p3 = new Person('ccc', 'female');
Person.getNum(); // 3  

#实现一个arrMerge 函数

实现一个arrMerge 函数,可传入2 个以上的数组类型参数,生成一个包含所有数组项,且没有重复项的新数组

function arrMerge() {
    var len = arguments.length,
        arr = [];
    for (var i = 0; i < len; i++) {
        // 合并
        arr = arr.concat(arguments[i]);
    }
    // 去重
    var result = [],
        hasElem = {};
    for (i = 0, l = arr.length; i < l; i++) {
        if (!hasElem[arr[i]]) {
            result.push(arr[i]);
            hasElem[arr[i]] = true;
            console.log(hasElem);
        }
    }
    return result;
}

实现可以接收任意个参数,我们需要了解js里面在function对象中arguments这个对象的知识,它代表此函数实参的参数列表,是一个类数组对象。

合并数组直接使用原生的concat()方法即可。

去重一步,使用了一个对象来记录此值是不是已经存在,使用对象来标识,效率比用数组来标识要高一点,因为对象是键值对的形式,类似哈希表,直接将数组元素作为此对象的键,用一个布尔值来标识这个数组元素是不是已经存在了,不存在则添加,并记录此元素已存在,存在则直接跳过。


arrMerge([0,1,1],[1,3],[2,0,456,6],[222,456]);
// [0, 1, 3, 2, 456, 6, 222]
arrMerge(['a', 'b', 'c', 'd'], ['a', 'bb', 'ccc', 'd'], ['11', 'sss']);
// ["a", "b", "c", "d", "bb", "ccc", "11", "sss"] 

#实现一个toCamelStyle函数

实现一个toCamelStyle 函数,把“aaa-bbb-cc”这种形式的命名转换为“aaaBbbCc”



function toCamelStyle(str) {
    var strArr = str.split('-'),
        temp = '',
        result = '';

    for (var i = 0, l = strArr.length; i < l; i++) {
        result += strArr[i].substr(0, 1).toUpperCase() + strArr[i].substr(1).toLowerCase();
        //console.log(result);
    }
    return result;
}

使用正则表达式完成


function toCamelStyle(str) {
    // 匹配-以及之后的一个字符,其中这个字符在一个分组内
    var camelRegExp = /-([a-z])/ig;

    return str.replace(camelRegExp, fcamelCase);

    // all为匹配到的内容,letter为组匹配
    function fcamelCase(all, letter) {
        console.log(all);
        console.log(letter);
        return letter.toUpperCase();
    }
}
toCamelStyle('aaa-bbb-cc');    // aaaBbbCc

使用正则表达式效率较高,之前的方法需要对整个字符串进行遍历,而正则表达式一次就把所有匹配内容获取到了,直接替换即可。

String.prototype.replace()方法第二个参数还可以是一个函数,接收多个参数,第一个为匹配到的内容,第二个为匹配到的分组,有多少组就可以传多少个参数,在此之后还可以有两个参数,一个为匹配到内容在原字符串的位置,另一个是原字符串。

以上在执行toCamelStyle(‘aaa-bbb-cc’)时,控制台输出结果分别为:-b b -c c,代表匹配到的内容为:-b 和 -c 对应的分组为:b c

#setTimeout实现重复调用

用setTimeout 实现一个定时循环任务,每隔200 毫秒,console 输出一句:”I am working …”


function showWorking() {
    var timer = timer || 1;
    console.log('I am working ...');
    // 避免重复调用 计时加快
    if (timer) clearTimeout(timer);
    timer = setTimeout(showWorking, 200);
}

setTimeout() 方法本来是迟延指定的时间执行指定的代码,要达到重复调用的效果就需要在方法里面加入它实现递归调用,从而达到效果。

setTimeout()setInterval()() 有所不同,后者是每隔指定的时间执行一次指定的代码,不需要递归就能重复调用。

但是后者不管执行的时间,只负责定时再次调用,比如指定100毫秒调用一次,那么每隔100ms就会发出一条指令,而不关心,上次的代码有没有执行完毕,假设所指定的代码执行需要一秒才能完成,那么一段时间后,会发现内存中会堆积很多等待执行的指令。 而前者本身就是迟延指定时间,在函数内部递归来实现重复调用,它会等待执行到它才会发出下一次指令,两次间隔的实际时间为执行时间+迟延时间(不考虑其他情况)。

#实现一个bind函数

实现一个bind 函数,传入一个函数和一个对象,返回一个新的函数,且传入对象为函数执行时的context,即this 的指向

ES6中可直接使用bind方法,类似call、apply,但是其返回一个改变上下文环境的新函数,而call和apply是替换上下文环境并运行原函数。


function bind(fun, context) {
    return fun.bind(context);
}

利用call或apply来实现一个

以下都是用apply而没有试用call的原因是因为,call第一个参数传递新的上下文环境,之后依次传入其他参数。而apply最多接受两个参数,第一个参数为新的上下文环境,第二个参数为数组(参数按顺序放入数组)。使用call需要将参数分割出来依次传递进去,而使用apply直接传递数组即可较为简单。


// 参数可在生成新函数时传递(即调用bind时),也可以在实际使用时传递
function bind(fun, context) {    
    var args = [].slice.call(arguments, 2);
    return function() {
        fun.apply(context || this, args.concat([].slice.call(arguments)));
    };
}

// 参数只能在生成新函数时传递
function bind1(fun, context) {    
    var args = [].slice.call(arguments, 2);
    return function() {
        fun.apply(context || this, args);
    };
}
// 参数只能在实际使用时传递
function bind2(fun, context) {
    return function() {
        fun.apply(context || this, [].slice.call(arguments));        
    };
}

调用测试:


var fun = function(sex, age) {
    console.log(this.name, sex, age);
};
var person = {
    name: "Andrew"
};

// 使用bind方法,可以在任何时候传递参数
var fun1 = bind(fun, person);
// 实际使用时传递
fun1('gril', 20); 					// Andrew gril 20
// 生成新函数时传递
bind(fun, person, 'gril', 20)(); 	// Andrew gril 20
// 混合传递
bind(fun, person, 'gril')(20); 		// Andrew gril 20

// bind1方法 只能在生成函数时传递 不支持调用时传递参数
bind1(fun, person, 'gril', 20)(); 		// Andrew gril 20
bind1(fun, person)('gril', 20); 		// Andrew undefined undefined
bind1(fun, person, 'gril')(20); 		// Andrew gril undefined

// bind2方法 只能在调用时传递,生成时传递无效
bind2(fun, person, 'gril', 20)(); 		// Andrew undefined undefined
bind2(fun, person)('gril', 20); 		// Andrew gril 20
bind2(fun, person, 'gril')(20); 		// Andrew 20 undefined

第一个方法是参照jQuery中$.proxy () 方法写的,之所以对参数进行了两次处理,原因在于,这样可以使得再调用bind方法生成新函数的时候,直接给原函数指定一些参数,达到固定前面一些参数的作用(之后传入的参数会依次后移,例如 bind(fun, person, ‘gril’)(‘boy’,20) 的结果为:Andrew gril boy,相当于在生成新函数的时候,直接把第一个参数固定为gril了,实际调用时候参数依次后移)。

第二个方法bind1几乎没有实际意义,仅仅是为了测试。因为根据原函数生成的新函数,不能传递参数了(参数只能在生成新函数的时候直接指定好)。

第三个方法bind2最符合简单的直接需求,bind2的作用仅仅是根据原函数,替换上下文,生成一个新函数,原函数的参数和新函数的参数相同。

#实现一个Utils模块

实现一个Utils 模块,有_method1 方法、_method2 方法、methodAll 方法,methodAll 中调用了_method1 和_method2


//  简单写法 
var Utils0 = {
    _method1: function() {
        console.log('this is _method1 running');
    },
    _method2: function() {
        console.log('this is _method1 running');
    },
    methodAll: function() {
        this._method1();
        this._method2();
    }
};

// 模块放大式写法
var Utils = (function(Utils) {
    var _method1 = function() {
            console.log('this is _method1 running');
        },
        _method2 = function() {
            console.log('this is _method1 running');
        },
        methodAll = function() {
            this._method1();
            this._method2();
        };
    return {
        _method1: _method1,
        _method2: _method2,
        methodAll: methodAll
    };
})(Utils || {});

可以简单写为一个对象,内部有几个方法的模式。但是这样,外部可以访问并修改这个对象的任何内容。

采用模块放大模式,对外暴露的仅仅是return出来的内容,在函数里面,可以定义很多私有的方法和属性。最后传递Utils || {}的作用是表示此部分代码可能仅是Utils模块的一部分,可做合并使用,多传入一个|| {}对象能去除加载顺序的依赖(当然要保证此块代码不依赖别的地方的Utils),此部分代码可以最先加载。

参考链接:面向对象编程模式

#输出一个对象自身的属性

有一个对象obj,请输出它自身具有的属性,而非它原型连上的。


function showOwnProp(obj) {
    if (typeof obj == "undefined" || typeof obj != 'object') throw new Error('请传入一个对象!');
    for (var key in obj) {
        // for in循环会遍历整个原型链 
        // in运算符返回一个布尔值,表示一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性。
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            console.log(key, ':', obj[key]);
        }
    }
}

其中 Object.prototype.hasOwnProperty.call(obj, key) 可以替换为 obj.hasOwnProperty(key) 之所以使用Object上的是因为防止obj对象上重写了 hasOwnProperty()方法对结果的影响。

另外在ES5 中可使用Object.keys方法和Object.getOwnPropertyNames方法 都返回数组,仅含自身属性,keys只返回可枚举的,而后者包含不可枚举的。

#对象深复制

在js 中,对象的赋值,实质是传递指向它内存的引用,请实现一个深度copy 的方法,传入一个对象obj,返回一个该对象的复制,而且两者没有任何值引用关联

复制对象需要保证:

  • 确保拷贝后的对象,与原对象具有同样的prototype原型对象。

  • 确保拷贝后的对象,与原对象具有同样的属性。

所以 1、原型链上的属性不要复制,直接指向即可。2、自身属性一一复制

下面总结了一点简单的复制对象方法:

  • 简单数组(内部不含符合类型)可直接使用slice方法

  • 不含json不支持的值(方法)以及enumerable属性不为false 的对象可转化为json字符串,再转化为对象。

  • 还可以直接及使用jQuery的extend方法,第一个参数传入true即可。

  • 不考虑不可枚举属性的话 可以遍历分别加入新对象即可。

以下演示通过属性描述对象拷贝对象。


// 在之前通过Person实例化出的zs对象上添加属性以做测试使用 
zs.family = {
    father: 'zsfather',
    mother: 'zsmother'
};
zs.children = [{}, {}];

function deepCopyObject(obj) {
    var copy = Object.create(Object.getPrototypeOf(obj));
    _copySelfProp(copy, obj);
    return copy;
    // 内部使用 拷贝自身属性
    function _copySelfProp(target, source) {
        Object
            .getOwnPropertyNames(source)
            .forEach(function(key) {
                console.log(key);
                // 获取属性描述对象
                var desc = Object.getOwnPropertyDescriptor(source, key);
                // 复合类型再次调用
                if (typeof desc.value == 'object') {
                    // function未处理,原因见下描述
                    target[key] = deepCopyObject(source[key]);
                } else {
                    // 将此属性添加到target
                    Object.defineProperty(target, key, desc);
                }
            });
        return target;
    }
}

先介绍两个方法

Object.defineProperty(obj, prop, descriptor)

obj 需要定义属性的对象。

prop 需定义或修改的属性的名字。

descriptor 将被定义或修改的属性的描述对象。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。该方法允许精确添加或修改对象的属性。一般情况下,我们为对象添加属性是通过赋值来创建并显示在属性枚举中(for…in 或 Object.keys 方法), 但这种方式添加的属性值可以被改变,也可以被删除。而使用 Object.defineProperty() 则允许改变这些额外细节的默认设置。例如,默认情况下,使用Object.defineProperty() 增加的属性值是不可改变的。

Object.getOwnPropertyDescriptor(obj, prop)

obj 要处理的对象

prop 属性名称,该属性的属性描述对象将被返回

该方法允许对一个属性的描述进行检索。在 Javascript 中, 属性 由一个字符串类型的“名字”(name)和一个“属性描述符”(property descriptor)对象构成。更多关于属性描述符类型以及他们属性的信息可以查看:Object.defineProperty

这个拷贝方法比把值(即使是简单类型的值)直接给新对象要精确很多。js中对象的值,可能看起来就是个字符串或者数值,但实际它还有一些属性,我们查看zs.name属性,发现他的描述对象为:Object {value: “zs”, writable: true, enumerable: true, configurable: true}。此方法拷贝的对象能保证这个值的属性也都和原对象一直。而直接赋值的方式,其他属性都变成了默认值。

参考链接:属性描述对象

但是getOwnPropertyDescriptordefinePropertygetOwnPropertyNames是在ES5和ES6中才有的,下面再展示一个只用ES写的


function deepCopy(obj) {
    // 通过原对象的构造函数来创建对象,确保类型一致且原型链相同
    var copy = obj.constructor.call();
    _copySelfProp(copy, obj);
    return copy;
    // 自身属性拷贝
    function _copySelfProp(target, source) {
        for (var key in source) {
            if (Object.prototype.hasOwnProperty.call(source, key)) {
                if (typeof source[key] == "object") {
                    // function未处理,原因见下描述
                    target[key] = deepCopy(source[key]);
                } else {
                    target[key] = source[key];
                }
            }
        }
        return target;
    }
}

需要指出的是,以上两个拷贝函数都没有对复合类型中的function进行处理(对象和数组进行typeof结果都为object),原因是函数一旦定义,不能对函数体进行修改,可以直接对齐进行引用。如果重新赋值一个新的函数给这个属性的话,由于新的函数也是一个对象,就切断了原来的联系,可以不用处理。

比如obj.a为一个function内存地址记为N1,对obj进行拷贝时,可以直接将obj1.a指向N1,如果修改obj.a为一个新的函数,此函数有一个内存地址N2,那么修改后:obj.a实际指向N2,而复制出的obj1.a指向N1。意思就是function比较特殊,不能像对象一样直接修改它内部的东西,可以直接拿来引用。

但是当前可以这么做的前提是:obj.a值仅仅是一个function,而没有其他值。实际可能存在的情况是先给obj.a=function(){},再接着给obj.a添加属性,obj.a.prop=[{},{}],(这就是js里面的一切皆对象,你甚至可以先var mm=‘111’,再mm.a=[{},{}],此时typeof mm 仍为string,但mm真的只是个字符串吗?)这种情况虽然不多,但是也是存在的,需要注意。

#使用字面量形式创建对象而不是构造函数

两者差异是因为其创建的时候不一样,构造函数是在运行时创建,而字面量形式是在编译时创建。

以下代码可以看出字面量形式创建对象效率要高很多,同时字面量形式创建对象,写的代码也少,而且比较可读。


console.time('for');
var arr10000=new Array(10000);
for(var i=0,l=arr10000.length;i<l;i++){
    arr10000[i]=new Object();
}
console.timeEnd('for'); //for: 4.885ms

console.time('for2');
var arr10000=new Array(10000);
for(var i=0,l=arr10000.length;i<l;i++){
    arr10000[i]={};
}
console.timeEnd('for2');//for2: 0.855ms

// 在创建正则表达式时,差别更加明显: 
console.time('for3');
var arr10000=new Array(10000);
for(var i=0,l=arr10000.length;i<l;i++){
    arr10000[i]=new RegExp('.*');
}
console.timeEnd('for3'); //for3: 10.689ms

console.time('for4');
var arr10000=new Array(10000);
for(var i=0,l=arr10000.length;i<l;i++){
    arr10000[i]=/.*/;
}
console.timeEnd('for4');//for4: 0.930ms