JavaScript数据类型及语言基础
1.// 判断arr是否为一个数组,返回一个bool值
function isArray(arr) {
var bool=arr instanceof Array;
return bool;
}
这是我自己最初写的函数,我刚开始看到这个题目首先想到的是typeof和Instanceof,但是typeof只能区分基本类型:number,string,undefined,boolean,object,function.
instanceof是用来确定一个值是哪种引用类型.所以当时我选择了instanceof,而且我看js高设好像也是用这个方法?
但是最后看到老师的review上面使用的是Object.prototype.toString方法.
这种判断方法更为精确,所以最后isArray()更正为:
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
2.// 判断fn是否为一个函数,返回一个bool值
function isFunction(fn){
return typeof(fn)==='function';
}
3.// 使用递归来实现一个深度克隆
可以复制一个目标对象,返回一个完整拷贝,被复制的对象类型会被限制为数字、字符串、布尔、日期、数组、Object对象。不会包含函数、正则对象等
// 测试用例:
var srcObj = {
a: 1,
b: {
b1: ["hello", "hi"],
b2: "JavaScript"
}
};
var abObj = srcObj;
var tarObj = cloneObject(srcObj);
srcObj.a = 2;
srcObj.b.b1[0] = "Hello";
console.log(abObj.a);
console.log(abObj.b.b1[0]);
console.log(tarObj.a); // 1
console.log(tarObj.b.b1[0]); // "hello"
思路:这里应该要分情况考虑,字符串,数字,布尔应该可以直接复制,因为是基本类型,存放在堆中,所以会创建一个新值。但是引用类型不同,如果直接复制,则传递过来的只是一个指针,并没有达到深度克隆的要求。所以,日期,数组,Object要分别考虑.代码如下:
function cloneObject(src){
if (!src||typeof src!="object") {
return src;
}
var clone;
if(src instanceof Date){
clone=new Date(src.getTime());
return clone;
}else if(src instanceof Array){
clone=[];
for(var i=0;i<src.length;i++){
clone[i]=cloneObject(src[i]);
}
return clone;
}else if(src instanceof Object){
/*
*第一种方法
clone={};
for(var x in src){
if(src.hasOwnProperty(x)){
clone[x]=cloneObject(src[x]);
}
}
return clone;*/
/*
*第二种方法
function obj(src){
function F(){}
F.prototype=src;
return new F();
}
return obj(src);*/
//第三种方法
return Object.create(src);
}
}
思路:首先判断是否为object,如果不是则直接返回值,对于Date,可以使用getTime()将值取出来,然后再赋给新的Date对象。
对于Array,直接创建一个新数组,然后遍历src,将src的每个值都赋给新数组.
对于object,我这里用了三种方法,第一种是for in判断是否是实例属性,第二种方法是采用原型式继承复制对象,最后一种则是使用ES5的Object.create()方法.
4.//学习数组、字符串、数字等相关方法,
//在util.js中实现以下函数
// 对数组进行去重操作,只考虑数组中元素为数字或字符串,返回一个去重后的数组
//第一种方法:
function uniqArray(arr){
var result=[];
for (var i = 0; i < arr.length; i++) {
if(result.indexOf(arr[i])==-1){
result.push(arr[i]);
}
}
return result;
}
//第二种方法:
function uniqArray2(arr){
var result={};
var ar=[];
for (var i = 0; i < arr.length; i++) {
if(!result[arr[i]]){
ar.push(arr[i]);
result[arr[i]]=true;
}
}
return ar;
}
思路:第一种方法是我自己写的,主要是利用数组的indexOf()方法循环查找arr数组里的每一项.
第二种方法是看到别人写的,这里采用了键值对的思想,创建一个对象,然后循环arr,检查对象的键,如果不存在,则push.
注意:数组最好不要用for in,因为这里面有坑,以前觉得方便老是用,但是有一次貌似遍历的时候多出来了几项,所以还是老老实实用for循环吧.
5.// 实现一个简单的trim函数,用于去除一个字符串,头部和尾部的空白字符
// 假定空白字符只有半角空格、Tab
// 练习通过循环,以及字符串的一些基本方法,分别扫描字符串str头部和尾部是否
//有连续的空白字符,并且删掉他们,最后返回一个完成去除的字符串
//第一种
function simpleTrim1(str){
var strr="";
if(str.charAt(0)==" "){
for (var i = 0; i < str.length; i++) {
if(str.charAt(i)!=" "){
var s=i;
strr=str.substring(s);
break;
}
}
}
if(strr.charAt(strr.length-1)==" "){
for(var j=strr.length-1;j>=0;j--) {
if(strr.charAt(j)!=" "){
var x=j;
strr=strr.substring(0,x+1);
break;
}
}
}
return strr;
}
//第二种:
function simpleTrim2(str){
var head=0,tail=str.length-1;
while(str[head]==" "){
head++;
}
while(str[tail]==" "){
tail--;
}
return str.substring(head,tail+1);
}
思路:第一种是利用字符串的charAt方法来判断首尾字符是否为空格,然后用循环标记出不是空格的首位字符以及第一位字符,然后用substring()来剪切字符串.但是我觉得应该没有这么复杂.
第二种就简洁了很多,用head,tail表示首尾,然后用while方法进行循环,求出head和tail的值,最后再一次性用substring方法剪切.
6.// 很多同学肯定对于上面的代码看不下去,接下来,我们真正实现一个trim
// 对字符串头尾进行空格字符的去除、包括全角半角空格、Tab等,返回一个字符串
// 尝试使用一行简洁的正则表达式完成该题目
function trim(str){
var pattern=/^\s+|\s+$/g;
//RegExp方法
if(pattern.test(str)){
//字符串替换方法
return str.replace(pattern,"");
}else{
return str;
}
}
思路:这里用到了正则表达式,还有正则的test()和string的replace()方法.
注意:正则里的“|”代表或运算符;
7./ 实现一个遍历数组的方法,针对数组中每一个元素执行fn函数,并将数组索引和元素作为参数传递
function each(arr, fn) {
// your implement
}
// 其中fn函数可以接受两个参数:item和index
// 使用示例
var arr = ['java', 'c', 'php', 'html'];
function output(item) {
console.log(item)
}
each(arr, output); // java, c, php, html
// 使用示例
var arr = ['java', 'c', 'php', 'html'];
function output(item, index) {
console.log(index + ': ' + item)
}
each(arr, output); // 0:java, 1:c, 2:php, 3:html
代码如下:
function each(arr,fn){
for(var i=0,len=arr.length;i<len;i++){
fn(arr[i],i);
}
}
思路:这题比较简单,就直接用一个循环,里面用一个fn()函数,然后传arr[i],i的值.
8.// 获取一个对象里面第一层元素的数量,返回一个整数
function getObjectLength(obj) {}
// 使用示例
var obj = {
a: 1,
b: 2,
c: {
c1: 3,
c2: 4
}
};
console.log(getObjectLength(obj)); // 3
代码如下:
//第一种:
function getObjectLength(obj){
var length=0;
for(var propName in obj){
if(obj.hasOwnProperty(propName)){
length++;
}
}
return length;
}
//第二种:
function getObjectLength(obj){
var length=0;
return Object.keys(obj).length;//键值
}
思路:for in语句会枚举出对象的所有属性,我们这里不需要全部属性,只需要对象的实例属性,所以还要用hasOwnProperty属性过滤一遍.
第二种:直接用Object.keys(obj)方法,返回的也是实例属性.但是貌似是es5里面的.
另外提一下,Object.getOwnPropertyNames(obj),可以返回obj的所有属性,但是好像也是es5的.
9.// 判断是否为邮箱地址
代码如下:
function isEmail(emailStr){
var pattern=/^\w+@\w+\.com/;
var i=emailStr.search(pattern)!==-1;
return i;
}
思路:我这里写的比较简单,没有考虑那么多.如果要复杂的正则的话我觉得网上有很多例子。
10.//判断是否为手机号
function isMobilePhone(phone){
var pattern=/^1\d{10}$/;
var i=phone.search(pattern)!==-1;
return i;
}
思路:同上,虽然比较简单,但是感觉手机号这样判断应该够了.
DOM
1.// 为element增加一个样式名为newClassName的新样式
代码如下:
function hasClass(element, className) {//首先判断elment有没有这个样式
var classNames = element.className;
if (!classNames) {
return false;
}
classNames = classNames.split(/\s+/);
for (var i = 0, len = classNames.length; i < len; i++) {
if (classNames[i] === className) {
return true;
}
}
return false;
}
function addClass(element,newClassName){
if(!hasClass(element,newClassName)){
element.className+=" "+newClassName;
}
}
思路:添加样式之前,首先要判断对象内是不是含有这个样式,采用正则将对象的原classname分隔开,循环判断。如果有,则返回true,没有,返回false.
然后再创建addClass(),调用上面的hasClass()方法,添加新样式.
2.// 移除element中的样式oldClassName
代码如下:
function removeClass(element,oldClassName){
if(hasClass(element.className)){
element.className.replace(pattern,"");
}
}
思路:同上,先调用hasClass()判断,然后再用replace()移除掉oldClassName.
3.// 判断siblingNode和element是否为同一个父元素下的同一级的元素,返回bool值
代码如下:
function isSimbingNode(element,siblingNode){
return element.parentNode===siblingNode.parentNode;
}
思路:刚开始我还想是不是用nextSibling还是previousSibling来判断,后面发现我有点逗了,直接用parentNode判断就可以了。
4.// 获取element相对于浏览器窗口的位置,返回一个对象{x, y}
代码如下:
function getPosition(element){
var x=0;
var y=0;
var current=element;
while(current!=null){
x+=current.offsetLeft;
y+=current.offsetTop;
current=current.offsetParent;
}
return{
x:x,
y:y
};
}
思路:这个题目刚开始感觉没什么思绪,后面在网上查了一点资料,才有点头绪,首先,来了解一下offsetLeft的概念,offsetLeft是指定对象相对于offsetParent的位置,而offsetParent是指布局中设置position属性的父容器.
通过while循环,offsetLeft累积叠加,直到offsetParent为null,这时,offsetLeft就是相对于版面的距离了,最后的结果就是element相对于浏览器窗口的位置。
接下来挑战一个mini $,它和之前的$是不兼容的,它应该是document.querySelector的功能子集,在不直接使用document.querySelector的情况下,在你的util.js中完成以下任务:
5.// 实现一个简单的Query
function $(selector) {
}
// 可以通过id获取DOM对象,通过#标示,例如
$("#adom"); // 返回id为adom的DOM对象
// 可以通过tagName获取DOM对象,例如
$("a"); // 返回第一个<a>对象
// 可以通过样式名称获取DOM对象,例如
$(".classa"); // 返回第一个样式定义包含classa的对象
// 可以通过attribute匹配获取DOM对象,例如
$("[data-log]"); // 返回第一个包含属性data-log的对象
$("[data-time=2015]"); // 返回第一个包含属性data-time且值为2015的对象
// 可以通过简单的组合提高查询便利性,例如
$("#adom .classa"); // 返回id为adom的DOM所包含的所有子节点中,第一个样式定义包含classa的对象
代码如下:
function $(selector) {
var ele = document;
var sele = selector.replace(/\s+/, ' ').split(' '); // 去除多余的空格并分割
for (var i = 0, len = sele.length; i < len; i++) {
switch (sele[i][0]) { // 查询第一个字符
case '#':
ele = ele.getElementById(sele[i].substring(1));
break;
case '.':
ele = ele.getElementsByClassName(sele[i].substring(1))[0];
break;
case '[':
var valueLoc = sele[i].indexOf('=');//标记'='的位置
var temp = ele.getElementsByTagName('*');//获取所有元素
var tLen = temp.length;
if (valueLoc !== -1) {
var key = sele[i].substring(1, valueLoc);
var value = sele[i].substring(valueLoc + 1, sele[i].length - 1);
for (var j = 0; j < tLen; j++) {
if (temp[j].getAttribute(key) === value) {
ele = temp[j];
break;
}
}
}
else {
var key = sele[i].substring(1, sele[i].length - 1);
for (var j = 0; j < tLen; j++) {
if (temp[j][key]) {
ele = temp[j];
break;
}
}
}
break;
default :
ele = ele.getElementsByTagName(sele[i])[0];
break;
}
}
if (!ele) {
ele = null;
}
return ele;
}
思路:这道题我感觉有点难度,刚开始我走偏了,用了switch来判断selector的第一个字符,发现有一个部分完成不了,$(“#adom .classa”); // 返回id为adom的DOM所包含的所有子节点中,第一个样式定义包含classa的对象,这个功能我并不能实现,因为只判断第一个元素。后面参考了别人的代码,首先用一个/\s+/配合replace方法将多余的空格替换为一个空格,然后用split方法分割为一个数组。
分割为数组之后,用for循环来一个个判断数组中元素的第一个字符。
‘[‘字符要分情况讨论,在含有’=’的情况下,要将’=’标记,然后取出前面的字符与后面的字符来判断。
4. 事件
我们来继续用封装自己的小jQuery库来实现我们对于JavaScript事件的学习,还是在你的util.js,实现以下函数
1.// 给一个element绑定一个针对event事件的响应,响应函数为listener
代码如下:
function addEvent(element,event,listener){
if (element.addEventListener) {
element.addEventListener(event,listener,false);
}else {
element.attachEvent("on"+event,listener);
}
}
思路:为了兼容ie8及以下,所以要用到attachEvent,而且attachEvent只支持事件冒泡,顺便吐槽一下,不知道兼容ie8及以下有什么意义啊,这都什么年月了…
2.// 移除element对象对于event事件发生时执行listener的响应.
代码如下:
function removeEvent(element,event,listener){
if (element.removeEventListener) {
element.removeEventListener(event,listener);
}else{
element.detachEvent("on"+event,listener);
}
}
思路:用removEventListener()来移除事件,但是要注意的是,removeEventListener()内传的参数必须和添加事件传的参数一致,并且,如果事件程序是匿名函数的话,不能通过removeEventListener移除.
3.// 实现对click事件的绑定
代码如下:
function addClickEvent(element,listener){
addEvent(element,'click',listener);
}
思路:调用上面的addEvent为element绑定click事件.
// 实现对于按Enter键时的事件绑定
代码如下:
function addEnterEvent(element, listener) {
addEvent(element,'keydown',function(event){
if (event.keyCode==13) {
listener.call(element,event);
}
});
}
思路:键盘事件对象event里有keyCode属性,判断event.keyCode就可以了.
值得注意的是,这里用到了call()方法,call()接收2个参数,第一个参数是this值,第二个参数是你想要传给函数的参数,也可以这么说,call()方法是用传入的对象来替换掉当前对象.所以,在上面的函数中,call()方法用element来替换掉本身的this值.
接下来我们把上面几个函数和$做一下结合,把他们变成$对象的一些方法
addEvent(element, event, listener) -> $.on(element, event, listener);
removeEvent(element, event, listener) -> $.un(element, event, listener);
addClickEvent(element, listener) ->$.click(element, listener);
addEnterEvent(element, listener) -> $.enter(element, listener);
代码如下:
$.on=addEvent;
$.un=removeEvent;
$.click=addClickEvent;
$.enter= addEnterEvent;
接下来考虑这样一个场景,我们需要对一个列表里所有的li增加点击事件的监听
最笨的方法
<ul id="list">
<li id="item1">Simon</li>
<li id="item2">Kenner</li>
<li id="item3">Erik</li>
</ul>
function clickListener(event) {
console.log(event);
}
$.click($("#item1"), clickListener);
$.click($("#item2"), clickListener);
$.click($("#item3"), clickListener);
上面这段代码要针对每一个item去绑定事件,这样显然是一件很麻烦的事情。
稍微好一些的
<ul id="list">
<li>Simon</li>
<li>Kenner</li>
<li>Erik</li>
</ul>
//我们试图改造一下
function clickListener(event) {
console.log(event);
}
each($("#list").getElementsByTagName('li'), function(li) {
addClickEvent(li, clickListener);
});
我们通过自己写的函数,取到id为list这个ul里面的所有li,然后通过遍历给他们绑定事件。这样我们就不需要一个一个去绑定了。但是看看以下代码:
<ul id="list">
<li id="item1">Simon</li>
<li id="item2">Kenner</li>
<li id="item3">Erik</li>
</ul>
<button id="btn">Change</button>
function clickListener(event) {
console.log(event);
}
function renderList() {
$("#list").innerHTML = '<li>new item</li>';
}
function init() {
each($("#list").getElementsByTagName('li'), function(item) {
$.click(item, clickListener);
});
$.click($("#btn"), renderList);
}
init();
事件代理
我们增加了一个按钮,当点击按钮时,改变list里面的项目,这个时候你再点击一下li,绑定事件不再生效了。那是不是我们每次改变了DOM结构或者内容后,都需要重新绑定事件呢?当然不会这么笨,接下来学习一下事件代理,然后实现下面新的方法:
代码如下:
function delegateEvent(element,tag,eventName,listener){
addEvent(element,eventName, function(event){
if (event.target&&event.target.nodeName.toLowerCase()==tag) {
listener.call(event.target,event);
}
});
}
$.delegate=delegateEvent;
思路:事件代理以前没接触过,翻阅一些资料后,发现这真是个好东西啊,因为以前写过一些Demo,当创建新的节点时,我必须将事件函数写入到创建节点的函数中来绑定,有了事件代理之后,我可以直接将事件绑定到父元素上面,事件代理的原理是利用了事件的冒泡机制,因为冒泡事件是从具体的元素向上传递直到window,所以我们可以将事件绑定到想要添加的元素的父元素上,在父元素上面来捕获事件,通过判断event.target.nodeName或者event.target.tagName来捕获事件,这里要提一下nodeName和tagName的区别,nodeName会显示所有节点的名称,而tagName只会显示元素节点的名称,但是他们都是输出的大写字母,例如:DIV.所以用toLowerCase()将他们转化.另外,这里也需要call()方法变更函数的作用域.