十个练习题目,感觉比较典型,分享一下。
累加函数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 类,有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这个对象的知识,它代表此函数实参的参数列表,是一个类数组对象。
合并数组直接使用原生的<code>concat()</code>方法即可。
去重一步,使用了一个对象来记录此值是不是已经存在,使用对象来标识,效率比用数组来标识要高一点,因为对象是键值对的形式,类似哈希表,直接将数组元素作为此对象的键,用一个布尔值来标识这个数组元素是不是已经存在了,不存在则添加,并记录此元素已存在,存在则直接跳过。
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
使用正则表达式效率较高,之前的方法需要对整个字符串进行遍历,而正则表达式一次就把所有匹配内容获取到了,直接替换即可。
<code>String.prototype.replace()</code>方法第二个参数还可以是一个函数,接收多个参数,第一个为匹配到的内容,第二个为匹配到的分组,有多少组就可以传多少个参数,在此之后还可以有两个参数,一个为匹配到内容在原字符串的位置,另一个是原字符串。
以上在执行<code>toCamelStyle('aaa-bbb-cc')</code>时,控制台输出结果分别为:-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);
}
<code>setTimeout()</code> 方法本来是迟延指定的时间执行指定的代码,要达到重复调用的效果就需要在方法里面加入它实现递归调用,从而达到效果。
<code>setTimeout()</code> 和<code>setInterval()()</code> 有所不同,后者是每隔指定的时间执行一次指定的代码,不需要递归就能重复调用。
但是后者不管执行的时间,只负责定时再次调用,比如指定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中<code>$.proxy ()</code> 方法写的,之所以对参数进行了两次处理,原因在于,这样可以使得再调用bind方法生成新函数的时候,直接给原函数指定一些参数,达到固定前面一些参数的作用(之后传入的参数会依次后移,例如 <code>bind(fun, person, 'gril')('boy',20)</code> 的结果为: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]);
}
}
}
其中 <code>Object.prototype.hasOwnProperty.call(obj, key)</code> 可以替换为 <code>obj.hasOwnProperty(key)</code> 之所以使用Object上的是因为防止obj对象上重写了 <code>hasOwnProperty()</code>方法对结果的影响。
另外在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}。此方法拷贝的对象能保证这个值的属性也都和原对象一直。而直接赋值的方式,其他属性都变成了默认值。
参考链接:属性描述对象
但是<code>getOwnPropertyDescriptor</code>、<code>defineProperty</code>、<code>getOwnPropertyNames</code>是在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
热门评论
虽然现在看不懂,不过我迟早会看懂的