十个练习题目,感觉比较典型,分享一下。
累加函数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命令,原理是这样的:
- 创建一个空对象,作为将要返回的对象实例
- 将这个空对象的原型,指向构造函数的prototype属性
- 将这个空对象赋值给函数内部的this关键字
- 开始执行构造函数内部的代码
更多关于原型和构造函数的具体知识请访问:面向对象编程概述
基于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}。此方法拷贝的对象能保证这个值的属性也都和原对象一直。而直接赋值的方式,其他属性都变成了默认值。
参考链接:属性描述对象
但是getOwnPropertyDescriptor、defineProperty、getOwnPropertyNames是在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