前端面试题目汇总摘录(JS 基础篇 —— 2018.11.01更新)
温故而知新,保持空杯心态
JS 基础
JavaScript 的 typeof 返回那些数据类型
object number function boolean undefined string
typeof null; // object
typeof isNaN; // function
typeof isNaN(123); //boolean
typeof []; // object
Array.isArray(); // false
toString.call([]); // [object Array]
var arr = [];
arr.constructor; // ƒ Array() { [native code] }
强制类型转换和隐式类型转换?
显示转换(强制类型转换)
js 提供了以下几种转型函数:
转换的类型 | 函数 |
---|---|
数值类型 | Number(mix),parseInt(string,radix),parseFloat(string); |
字符串类型 | toString(radix),String(mix) |
布尔类型 | Boolean(mix) |
Number(mix) 函数,可以将任意类型的参数 mix 转换为数值类型,规则为
- 如果是布尔值,
true
和false
分别被转换为 1 和 0 - 如果是数字值,返回本身
- 如果是
null
,返回 0 - 如果是
undefined
,返回NaN
- 如果是字符串,遵循以下规则:
- 如果字符串中只包含数字,则将其转换为十进制(忽略前导0,前面正负号有效)
- 如果字符串中包含有效的浮点格式,则将其转换为对应的浮点数值(忽略前导0,前面正负号有效)
- 如果字符串中包含有效的十六进制格式,则转换为相同大小的十进制整数值
- 如果字符串是空的(不包含任何字符),则将其转换为 0
- 如果字符串中包含上述格式之后的字符,则将其转换为 NaN
- 如果是对象,则调用对象的
valueOf()
方法,然后按照前面的规则进行转换返回的值,如果是转换结果是NaN
,则调用对象的toString()
方法,然后再一次按照前面的规则进行返回的字符串值的转换
下表是对象的 valueOf() 的返回值
对象 | 返回值 |
---|---|
Array | 数组的元素被转换为字符串,这些字符串由逗号分隔,连接在一起。其操作与 Array.toString 和 Array.join 方法相同。 |
Boolean | Boolean 值。 |
Date | 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。 |
Function | 函数本身。 |
Number | 数字值。 |
Object | 对象本身。这是默认情况。 |
String | 字符串值。 |
由于 Number()函数在转换字符串时原理比较复杂,且不够合理,因此在处理字符串时,更常用的是 parseInt()
函数
parstInt(string,radix) 函数,将字符串转换为整数类型的数值,其规则为
- 忽略前面字符串前面的空格,直至找到第一个非空格字符
- 如果第一个字符不是数字字符或者负号,就会返回
NaN
(也就是遇到空字符会返回 NaN) - 如果第一个字符是数字字符,会继续解析第二个字符,知道解析完所有后续的字符或者是遇到一个非数字字符
- 如果字符串中第一个字符是数字字符,也能够识别各种进制
- 最好在 第二个参数指定转换的基数(进制),就不会有所歧义。
parseFloat(string)函数,将字符串转换为浮点数类型的数值。
与parseInt()函数类似,parseFloat()也是从第一个字符(位置0)开始解析每个字符。而且也是一直解析到字符串末尾,或者解析到遇见一个无效的浮点数字字符为止。也就是说,字符串中的第一个小数点是有效的,而第二个小数点就是无效的了,因此它后面的字符串将被忽略。
toString(radix)
除 undefined
和 null
之外的所有类型的值都具有 toString()
方法,其作用是返回对象的字符串表示。
多数情况下,调用toString()方法不必传递参数。但是,在调用数值的toString()方法时,可以传递一个参数:输出数值的基数。默认情况下,toString()方法以十进制格式返回数值的字符串表示。
对象 | 操作 |
---|---|
Array | 将 Array 的元素转换为字符串。结果字符串由逗号分隔,且连接起来。 |
Boolean | 如果 Boolean 值是 true,则返回 “true”。否则,返回 “false”。 |
Date | 返回日期的文字表示法。 |
Error | 返回一个包含相关错误信息的字符串。 |
Function | 返回如下格式的字符串,其中 functionname 是被调用 toString 方法函数的名称:function functionname( ) { [native code] } |
Number | 返回数字的文字表示。 |
String | 返回 String 对象的值。 |
默认 | 返回 “[object objectname]”,其中 objectname 是对象类型的名称。 |
在不知道要转换的值是不是null或undefined的情况下,还可以使用转型函数String(),这个函数能够将任何类型的值转换为字符串。
String(mix)函数,将任何类型的值转换为字符串,其规则为:
- 如果有toString()方法,则调用该方法(不传递radix参数)并返回结果
- 如果是null,返回”null”
- 如果是undefined,返回”undefined”
Boolean(mix)函数,将任何类型的值转换为布尔值。
以下值会被转换为false
:false、”"、0、NaN、null、undefined,其余任何值都会被转换为true
。
隐式转换(非强制转换类型)
在某些情况下,即使我们不提供显示转换,Javascript也会进行自动类型转换,主要情况有:
用于检测是否为非数值的函数:isNaN(mix)
isNaN()
函数,经测试发现,该函数会尝试将参数值用 Number()
进行转换,如果结果为“非数值”则返回 true
,否则返回 false
。
递增递减操作符(包括前置和后置)、一元正负符号操作符(经过对比发现,其规则与Number()规则基本相同)
- 如果是包含有效数字字符的字符串,先将其转换为数字值(转换规则同
Number()
),再执行加减1的操作,字符串变量变为数值变量。 - 如果是不包含有效数字字符的字符串,将变量的值设置为
NaN
,字符串变量变成数值变量。 - 如果是布尔值
false
,先将其转换为0再执行加减1的操作,布尔值变量编程数值变量。 - 如果是布尔值
true
,先将其转换为1再执行加减1的操作,布尔值变量变成数值变量。 - 如果是浮点数值,执行加减1的操作。
- 如果是对象,先调用对象的
valueOf()
方法,然后对该返回值应用前面的规则。如果结果是NaN
,则调用toString()
方法后再应用前面的规则。对象变量变成数值变量。
加法运算操作符
加号运算操作符在Javascript也用于字符串连接符,所以加号操作符的规则分两种情况:
如果两个操作值都是数值,其规则为:
- 如果一个操作数为
NaN
,则结果为NaN
- 如果是
Infinity+Infinity
,结果是Infinity
- 如果是
-Infinity+(-Infinity)
,结果是-Infinity
- 如果是
Infinity+(-Infinity)
,结果是NaN
- 如果是
+0+(+0)
,结果为+0
- 如果是
(-0)+(-0)
,结果为-0
- 如果是
(+0)+(-0)
,结果为+0
如果有一个操作值为字符串,则:
- 如果两个操作值都是字符串,则将它们拼接起来 如果只有一个操作值为字符串,则将另外操作值转换为字符串,然后拼接起来
- 如果一个操作数是对象、数值或者布尔值,则调用toString()方法取得字符串值,然后再应用前面的字符串规则。
- 对于undefined和null,分别调用String()显式转换为字符串。
可以看出,加法运算中,如果有一个操作值为字符串类型,则将另一个操作值转换为字符串,最后连接起来。
乘除、减号运算符、取模运算符
这些操作符针对的是运算,所以他们具有共同性:如果操作值之一不是数值,则被隐式调用Number()
函数进行转换。具体每一种运算的详细规则请参考ECMAScript中的定义。
逻辑操作符(!、&&、||)
逻辑非(!)操作符首先通过Boolean()函数将它的操作值转换为布尔值,然后求反。
逻辑与(&&)操作符,如果一个操作值不是布尔值时,遵循以下规则进行转换:
- 如果第一个操作数经
Boolean()
转换后为true
,则返回第二个操作值,否则返回第一个值(不是Boolean()
转换后的值) - 如果有一个操作值为
null
,返回null
- 如果有一个操作值为
NaN
,返回NaN
- 如果有一个操作值为
undefined
,返回undefined
逻辑或(||)操作符,如果一个操作值不是布尔值,遵循以下规则
- 如果第一个操作值经
Boolean()
转换后为false
,则返回第二个操作值,否则返回第一个操作值(不是Boolean()
转换后的值) - 对于
undefined
、null
和NaN
的处理规则与逻辑与(&&)相同
关系操作符(<, >, <=, >=)
与上述操作符一样,关系操作符的操作值也可以是任意类型的,所以使用非数值类型参与比较时也需要系统进行隐式类型转换:
- 如果两个操作值都是数值,则进行数值比较
- 如果两个操作值都是字符串,则比较字符串对应的字符编码值
- 如果只有一个操作值是数值,则将另一个操作值转换为数值,进行数值比较
- 如果一个操作数是对象,则调用
valueOf()
方法(如果对象没有valueOf()
方法则调用toString()
方法),得到的结果按照前面的规则执行比较 - 如果一个操作值是布尔值,则将其转换为数值,再进行比较
注:NaN
是非常特殊的值,它不和任何类型的值相等,包括它自己,同时它与任何类型的值比较大小时都返回 false
。
相等操作符(==)
相等操作符会对操作值进行隐式转换后进行比较:
- 如果一个操作值为布尔值,则在比较之前先将其转换为数值
- 如果一个操作值为字符串,另一个操作值为数值,则通过Number()函数将字符串转换为数值
- 如果一个操作值是对象,另一个不是,则调用对象的
valueOf()
方法,得到的结果按照前面的规则进行比较 null
与undefined
是相等的- 如果一个操作值为
NaN
,则相等比较返回false
- 如果两个操作值都是对象,则比较它们是不是指向同一个对象
split()、join()的区别
前者是切割成数组的形式
后者是将数组转换为字符串
数组方法pop/push/unshift/shift
数组方法 | 描述 |
---|---|
pop() | 删除原数组最后一项,并返回删除元素的值;如果数组为空则返回undefined |
push() | 将参数添加到原数组末尾,并返回数组的长度 |
unshift() | 将参数添加到原数组开头,并返回数组的长度 |
shift() | 删除原数组第一项,并返回删除元素的值;如果数组为空则返回undefined |
事件绑定和普通事件有什么区别
普通事件中的onclick是DOM0级事件只支持单个事件,会被其他onclick事件覆盖,而事件绑定中的addEventListener是DOM2级事件可以添加多个事件而不用担心被覆盖
普通添加事件的方法:
var btn = document.getElementById("hello");
btn.onclick = function(){alert(1);
}
btn.onclick = function(){alert(2);
}
执行上面的代码只会alert 2
事件绑定方式添加事件:
var btn = document.getElementById("hello");
btn.addEventListener("click",function(){alert(1);
},false);
btn.addEventListener("click",function(){alert(2);
},false);
执行上面的代码会先alert 1 再 alert 2
IE 和 DOM 事件流有什么区别
事件
HTML元素事件是浏览器内在自动产生的,当有事件发生时html元素会向外界(这里主要指元素事件的订阅者)发出各种事件,如click,onmouseover,onmouseout等等。
DOM事件流
DOM(文档对象模型)结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素结点与根结点之间的路径传播,路径所经过的结点都会收到该事件,这个传播过程可称为DOM事件流。
冒泡型事件(Bubbling)
这是IE浏览器对事件模型的实现。冒泡,顾名思义,事件像个水中的气泡一样一直往上冒,直到顶端。从DOM树型结构上理解,就是事件由叶子结点沿祖先结点一直向上传递直到根结点;从浏览器界面视图HTML元素排列层次上理解就是事件由具有从属关系的最确定的目标元素一直传递到最不确定的目标元素.
捕获型事件(Capturing)
Netscape Navigator的实现,它与冒泡型刚好相反,由DOM树最顶层元素一直到最精确的元素,直观上的理解应该如同冒泡型,事件传递应该由最确定的元素,即事件产生元素开始。
DOM标准事件模型
因为两个不同的模型都有其优点和解释,DOM标准支持捕获型与冒泡型,可以说是它们两者的结合体。它可以在一个DOM元素上绑定多个事件处理器,并且在处理函数内部,this关键字仍然指向被绑定的DOM元素,另外处理函数参数列表的第一个位置传递事件event对象。
首先是捕获式传递事件,接着是冒泡式传递,所以,如果一个处理函数既注册了捕获型事件的监听,又注册冒泡型事件监听,那么在DOM事件模型中它就会被调用两次。
实例:
<body><div><button>点击这里</button></div>
</body>
冒泡:button
-> div
-> body
(IE 事件流)
捕获:body
-> div
-> button
(Netscape事件流)
DOM: body
-> div
-> button
-> button
-> div
-> body
(先捕获后冒泡)
事件侦听函数的区别
// IE使用:
[Object].attachEvent("name_of_event_handler", fnHandler); //绑定函数
[Object].detachEvent("name_of_event_handler", fnHandler); //移除绑定 // DOM使用:
[Object].addEventListener("name_of_event", fnHandler, bCapture); //绑定函数
[Object].removeEventListener("name_of_event", fnHandler, bCapture); //移除绑定
如何取消浏览器事件的传递与事件传递后浏览器的默认处理
取消事件传递是指,停止捕获型事件或冒泡型事件的进一步传递。
事件传递后的默认处理是指,通常浏览器在事件传递并处理完后会执行与该事件关联的默认动作(如果存在这样的动作)。例如,如果表单中input type 属性是 “submit”,点击后在事件传播完浏览器就就自动提交表单。又例如,input 元素的 keydown 事件发生并处理后,浏览器默认会将用户键入的字符自动追加到 input 元素的值中。
要取消浏览器的事件传递,IE与DOM标准又有所不同。
在IE下,通过设置 event
对象的 cancelBubble
为 true
即可。
function someHandle() { window.event.cancelBubble = true;
}
DOM标准通过调用 event
对象的 stopPropagation()
方法即可。
function someHandle(event) {event.stopPropagation();
}
因些,跨浏览器的停止事件传递的方法是:
function someHandle(event) { event = event || window.event; if(event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true;
}
取消事件传递后的默认处理,IE与DOM标准又不所不同。
在IE下,通过设置 event
对象的 returnValue
为 false
即可。
function someHandle() { window.event.returnValue = false;
}
DOM标准通过调用 event
对象的 preventDefault()
方法即可。
function someHandle(event) { event.preventDefault();
}
因些,跨浏览器的取消事件传递后的默认处理方法是:
function**` `someHandle(event) {
event = event || window.event;
if(event.preventDefault) event.preventDefault(); else event.returnValue = false;
}
IE 和标准下有哪些兼容性的写法
var ev = ev || window.event
document.documentElement.clinetWidth || document.body.clientWidth
var target = ev.srcElement || ev.target
call 和 apply 的区别
call 和 apply 相同点:
都是为了用一个本不属于一个对象的方法,让这个对象去执行
基本使用
call()
function.call(obj[,arg1[, arg2[, [,.argN]]]]])
- 调用
call
的对象必须是个函数function call
的第一个参数将会是function改变上下文后指向的对象.如果不传,将会默认是全局对象window
- 第二个参数开始可以接收任意个参数,这些参数将会作为function的参数传入function
- 调用
call
的方法会立即执行
apply()
function.apply(obj[,argArray])
与call
方法的使用基本一致,但是只接收两个参数,其中第二个参数必须是一个数组或者类数组,这也是这两个方法很重要的一个区别
数组与类数组小科普
数组我们都知道是什么,它的特征都有哪些呢?
- 可以通过角标调用,如
array[0]
- 具有长度属性
length
- 可以通过 for 循环和
forEach
方法进行遍历
类数组顾名思义,具备的特征应该与数组基本相同,那么可以知道,一个形如下面这个对象的对象就是一个类数组
var arrayLike = {0: 'item1',1: 'item2',2: 'item3',length: 3
}
类数组arrayLike
可以通过角标进行调用,具有length
属性,同时也可以通过 for 循环进行遍历
我们经常使用的获取dom节点的方法返回的就是一个类数组,在一个方法中使用 arguments
关键字获取到的该方法的所有参数也是一个类数组
但是类数组却不能通过forEach
进行遍历,因为forEach
是数组原型链上的方法,类数组毕竟不是数组,所以无法使用
不同点
call
方法从第二个参数开始可以接收任意个参数,每个参数会映射到相应位置的func的参数上,可以通过参数名调用,但是如果将所有的参数作为数组传入,它们会作为一个整体映射到func对应的第一个参数上,之后参数都为空
function func (a,b,c) {}func.call(obj, 1,2,3)
// function接收到的参数实际上是 1,2,3func.call(obj, [1,2,3])
// function接收到的参数实际上是 [1,2,3],undefined,undefined
apply
方法最多只有两个参数,第二个参数接收数组或者类数组,但是都会被转换成类数组传入func中,并且会被映射到func对应的参数上
func.apply(obj, [1,2,3])
// function接收到的参数实际上是 1,2,3func.apply(obj, {0: 1,1: 2,2: 3,length: 3
})
// function接收到的参数实际上是 1,2,3
b 继承 a 的方法
方法一:对象冒充
function Parent(username){this.username = username;this.hello = function(){console.log(this.username);}
}
function Child(username,password){this.method = Parent; // this.method 作为一个临时的属性,并且指向了 Parent所指向的对象函数this.method(username); // 执行 this.method 方法,即执行了 Parent 所指向的对象函数delete this.method; // 销毁 this.method 属性,即此时 Child 就已经拥有了 Parent 的所有方法和属性 this.password = password;this.world = function(){console.log(this.password);}
}
const parent = new Parent('hello parent');
const child = new Child('hello child','123456');
console.log(child);
parent.hello();
child.hello();
child.world();
方法二:call()
call 方法是 Function 类中的方法
call 方法的第一个参数的值赋值给类(即方法)中出现的 this
call 方法的第二个参数开始依次赋值给类(即方法)所接受的参数
function Parent(username){this.username = username;this.hello = function(){console.log(this.username);}
}
function Child(username,password){Parent.call(this,username);this.password = password;this.world = function(){console.log(this.password);}
}
const parent = new Parent('hello parent');
const child = new Child('hello child','123456');
parent.hello();
child.hello();
child.world();
方法三:apply()
apply方法接受2个参数
第一个参数与call方法的第一个参数一样,即赋值给类(即方法)中出现的this
第二个参数为数组类型,这个数组中的每个元素依次赋值给类(即方法)所接受的参数
function Parent(username){this.username = username;this.hello = function(){console.log(this.username);}
}
function Child(username,password){Parent.apply(this,new Array(username));this.password = password;this.world = function(){console.log(this.password);}
}
const parent = new Parent('hello parent');
const child = new Child('hello child','123456');
parent.hello();
child.hello();
child.world();
方法四:原型链
即子类通过 prototype 将所有在父类中通过 prototype 追加的属性和方法都追加到 Child ,从而实现继承
function Parent(){}
Parent.prototype.hello = "hello";
Parent.prototype.sayHello = function(){console.log(this.hello);
}
function Child(){}
Child.prototype = new Parent();// 将 Parent 中所有通过 prototype 追加的属性和方法都追加到 Child 从而实现了继承
Child.prototype.world = "world";
Child.prototype.sayWorld = function(){console.log(this.world);
}
const child = new Child();
child.sayHello();
child.sayWorld();
方法五:混合方式,call()+ 原型链
function Parent(hello){this.hello = hello;
}
Parent.prototype.sayHello = function(){console.log(this.hello);
}
function Child(hello,world){Parent.call(this,hello); // 将父类的属性继承过来this.world = world;
}
Child.prototype = new Parent(); //将父类的方法继承过来
Child.prototype.sayWorld = function(){ // 新增方法console.log(this.world);
}
const child = new Child("hello","world");
child.sayHello();
child.sayWorld();
JavaScript this 指针、闭包、作用域
js 中的this 指针
在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。
在《javaScript语言精粹》这本书中,把 this 出现的场景分为四类,简单的说就是:
1)有对象就指向调用对象
var myObject = { value: 123 }
myObject.getValue = function(){console.log(this.value); // 123console.log(this); // {value: 123, getValue: ƒ}
}
myObject.getValue();
2)没调用对象就指向全局对象
var myObject = { value: 123 }
myObject.getValue = function(){var foo = function(){console.log(this.value); // undefinedconsole.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …} // foo函数虽然定义在getValue 函数体内,但是不属于 getValue也不属于 myObject,所以调用的时候,它的 this 指针指向了全局对象}foo();return this.value;
}
console.log(myObject.getValue()); // 123
3) 用new构造就指向新对象
// js 中通过 new 关键词来调用构造函数,此时 this 会绑定杂该新对象上
var someClass = function(){this.value = 123;
}
var myCreate = new someClass();
console.log(myCreate.value); // 123
4)通过 apply 或 call 或 bind 来改变 this 的指向
var myObject = { value: 123 };
var foo = function(){console.log(this);
}
foo(); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
foo.apply(myObject); // {value: 123}
foo.call(myObject); // {value: 123}
var newFoo = foo.bind(myObject);
newFoo(); // {value: 123}
闭包
闭包英文是 Closure ,简而言之,闭包就是
- 函数的局部集合,只是这些局部变量在函数返回后会继续存在
- 函数的“堆栈”在函数返回后并不释放,可以理解为这些函数堆栈并不在栈上分配而是在堆上分配
- 当在一个函数内部定义另外一个函数就会产生闭包
作为局部变量都可以被函数内的代码访问,这个和静态语言是没有差别的,闭包的差别在于局部变量可以在函数执行结束后仍然被函数外的代码访问,这意味着函数必须返回一个指向闭包的“引用”,或将这个“引用”赋值给某个外部变量,才能保证闭包中局部变量被外部代码访问,当然包含这个引用的实体应该是一个对象。但是ES并没有提供相关的成员和方法来访问包中的局部变量,但是在ES中,函数对象中定义的内部函数是可以直接访问外部函数的局部变量,通过这种机制,可以用如下方式完成对闭包的访问。
function greeting(name){var text = "Hello " + name; // 局部变量// 每次调用时,产生闭包,并返回内部函数对象给调用者return function(){console.log(text);}
}
var sayHello = greeting('Closure');
// 通过闭包访问到了局部变量text
sayHello(); // 输出Hello Closure
在 ECMAscript 的脚本函数运行时,每个函数关联都有一个执行上下文场景(Exection Context),这个执行上下文包括三个部分
- 文法环境(The LexicalEnvironment)
- 变量环境(The VariableEnvironment)
- this绑定
其中第三点this绑定与闭包无关,不在本文中讨论。文法环境中用于解析函数执行过程使用到的变量标识符。我们可以将文法环境想象成一个对象,该对象包含了两个重要组件,环境记录(Enviroment Recode),和外部引用(指针)。环境记录包含包含了函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。全局的上下文场景中此引用值为NULL。这样的数据结构就构成了一个单向的链表,每个引用都指向外层的上下文场景。
例如上面我们例子的闭包模型应该是这样,sayHello函数在最下层,上层是函数greeting,最外层是全局场景。如下图:
因此当sayHello被调用的时候,sayHello会通过上下文场景找到局部变量text的值,因此在屏幕的对话框中显示出”Hello Closure”
针对一些例子来帮助大家更加深入的理解闭包,下面共有5个样例,例子来自于JavaScript Closures For Dummies(镜像)。
例子1:闭包中局部变量是引用而非拷贝
function say667(){var num = 666;var sayConsole = function(){console.log(num);}num++;return sayConsole;
}
var sayConsole = say667();
sayConsole(); // 667
例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。
function setupSomeGlobals(){var num = 666;gConsoleNumber = function() { console.log(num); }gIncreaseNumber = function() { num++; }gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gConsoleNumber(); // 666
gIncreaseNumber();
gConsoleNumber(); // 667
gSetNumber(12);
gConsoleNumber(); // 12
例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包
function buildList(list){var result = [];for(var i = 0; i < list.length; i++){var item = 'item' + list[i];result.push(function(){console.log(item+' '+list[i]);})}return result;
}
function testList(){var fnList = buildList([1,2,3]);for(var j = 0; j < fnList.length; j++){fnList[j]();}
}
testList(); // 输出3次 item3 undefined
testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.
例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。
function sayAlice(){var sayConsole = function(){console.log(alice);}var alice = "Hello Alice";return sayConsole;
}
var helloAlice=sayAlice();
helloAlice();
执行结果输出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。
例子5:每次函数调用的时候创建一个新的闭包
function newClosure(someNum,someRef){var num = someNum;var anArray = [1,2,3];var ref = someRef;return function(x){num += x;anArray.push(num);console.log('num: ' + num +'\nanArray ' + anArray.toString() +'\nref.someVar ' + ref.someVar);}
}closure1=newClosure(40,{someVar:'closure 1'});
closure2=newClosure(1000,{someVar:'closure 2'});closure1(5); // num: 45 anArray 1,2,3,45 ref.someVar closure 1
closure2(-10); // num: 990 anArray 1,2,3,990 ref.someVar closure 2
闭包的缺点:
(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
例子:
function Cars(){this.name = "Benz";this.color = ["white","black"];
}
Cars.prototype.sayColor = function(){var outer = this;return function(){return outer.color};
};var instance = new Cars();
console.log(instance.sayColor()())
改造:
function Cars(){this.name = "Benz";this.color = ["white","black"];
}
Cars.prototype.sayColor = function(){var outerColor = this.color; //保存一个副本到变量中return function(){return outerColor; //应用这个副本};outColor = null; //释放内存
};var instance = new Cars();
console.log(instance.sayColor()())
作用域
在JS当中一个变量的作用域(scope)是程序中定义这个变量的区域。变量分为两类:全局(global)的和局部的。其中全局变量的作用域是全局性的,即在JavaScript代码中,它处处都有定义。而在函数之内声明的变量,就只在函数体内部有定义。它们是局部变量,作用域是局部性的。函数的参数也是局部变量,它们只在函数体内部有定义。
我们可以借助JavaScript的作用域链(scope chain)更好地了解变量的作用域。每个JavaScript执行环境都有一个和它关联在一起的作用域链。这个作用域链是一个对象列表或对象链。当JavaScript代码需要查询变量x的值时(这个过程叫做变量解析(variable name resolution)),它就开始查看该链的第一个对象。如果那个对象有一个名为x的属性,那么就采用那个属性的值。如果第一个对象没有名为x的属性,JavaScript就会继续查询链中的第二个对象。如果第二个对象仍然没有名为x的属性,那么就继续查询下一个对象,以此类推。如果查询到最后(指顶层代码中)不存在这个属性,那么这个变量的值就是未定义的。
var a,b;
(function(){alert(a); // undefined alert(b); // undefined var a = b = 3;alert(a); // 3alert(b); // 3
})();alert(a); // undefined alert(b); // 3
以上代码相当于
var a,b;
(function(){alert(a);alert(b);var a = 3;b = 3;alert(a);alert(b);
})();alert(a);
事件委托是什么?
概述
什么叫做事件委托,别名叫事件代理,JavaScript 高级程序设计上讲。事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
实际例子:
有三个同事预计会在周一收到快递,为签收快递,有两种方法:一是三个人在公司门口等快递,二是委托给前台的小姐代为签收。现实生活中,我们大多采用委托的方案(公司也不会容忍那么多人站在门口)。前台小姐收到快递后,会判断收件人是谁,按照收件人的要求签收,甚至是代付。这种方案还有一个好处就是,即使公司来了很多新员工(不管多少),前台小姐也会在收到寄给新员工们的快递后核实代为签收。
这里有2层意思:
第一、现在委托前台的小姐是可以代为签收的,即程序中的现有的 DOM 节点是有事件的。
第二、新员工也可以被前台小姐代为签收,即程序中新添加的 DOM 节点也是有事件的。
为什么要使用事件委托
一般来说,DOM 需要有事件处理程序,就会直接给它设处理程序,但是如果是很多 DOM 需要添加处理事件呢?例如我们有100个 li,每个 li 都有相同的 click 点击事件,可能我们会用到 for 循环,来遍历所有 li ,然后给它们添加事件,那么会存在什么样的问题?
在 JavsScript 中,添加到页面上的事件处理程序数量将直接影响到整体运行性能,因为需要不断地与 DOM 进行交互,访问 DOM 的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少 DOM 操作的原因、如果要用到事件委托,就会将所有的操作都放在 js 程序里面,与 DOM 的操作就只需要交互一次,这样就可以大大减少与 DOM 的交互次数,提高性能。
每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了(内存不够用,是硬伤,哈哈),比如上面的100个li,就要占用100个内存空间,如果是1000个,10000个呢,那只能说呵呵了,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,自然性能就会更好。
事件委托的原理
事件委托是利用事件的冒泡原理来实现的,何为事件冒泡?就是事件从最深的节点开始执行,然后逐步向上传播事件,例子:
页面上有一个节点树,div>ul>li>a,比如给最里面的 a 加一个 click 点击事件,那么这个事件就会一层一层的往外执行,执行顺序 a>li>ul>div,有这么一个机制,那么我们给最外面的 div 加点击事件,那么里面的 ul,li,a 做点击事件的时候,都会冒泡到最外层的 div 上面,都会触发,这就是事件委托,委托他们父级代为执行事件。
事件委托怎么实现
<ul id="ul"><li>111</li><li>222</li><li>333</li><li>444</li>
</ul>
实现功能是点击li,弹出123:
window.onload = function(){var oUl = document.getElementById('ul');var aLi = oUl.getElementsByTagName('li');for(var i = 0; i < aLi.length; i++){aLi[i].onclick = function(){alert(123);}}
}
上面的代码的意思很简单,相信很多人都是这么实现的,我们看看有多少次的dom操作,首先要找到ul,然后遍历li,然后点击li的时候,又要找一次目标的li的位置,才能执行最后的操作,每次点击都要找一次li;
那么我们用事件委托的方式做又会怎么样呢?
window.onload = function(){var oUl = document.getElementById('ul');oUl.onclick = function(){alert(123);}
}
这里用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发,当然,这里当点击ul的时候,也是会触发的,那么问题就来了,如果我想让事件代理的效果跟直接给节点的事件效果一样怎么办,比如说只有点击li才会触发,不怕,我们有绝招:
Event对象提供了一个属性叫target,可以返回事件的目标节点,我们成为事件源,也就是说,target就可以表示为当前的事件操作的dom,但是不是真正操作dom,当然,这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较(习惯问题):
window.onload = function(){var oUl = document.getElementById("ul");oUl.onclick = function(ev){var ev = ev || window.event;var target = ev.target || ev.srcElement;if(target.nodeName.toLowerCase() == "li"){alert(target.innerHTML);}}
}
这样改下就只有点击li会触发事件了,且每次只执行一次dom操作,如果li数量很多的话,将大大减少dom的操作,优化的性能可想而知!
上面的例子是说li操作的是同样的效果,要是每个li被点击的效果都不一样,那么用事件委托还有用吗?
var Add = document.getElementById("add");
var Remove = document.getElementById("remove");
var Move = document.getElementById("move");
var Select = document.getElementById("select");
Add.onclick = function(){alert('添加');
};
Remove.onclick = function(){alert('删除');
};
Move.onclick = function(){alert('移动');
};
Select.onclick = function(){alert('选择');
}
上面实现的效果我就不多说了,很简单,4个按钮,点击每一个做不同的操作,那么至少需要4次dom操作,如果用事件委托,能进行优化吗?
var oBox = document.getElementById("box");
oBox.onclick = function(ev){var ev = ev || window.event;var target = ev.target || ev.srcElement;if(target.nodeName.toLowerCase() == 'input'){switch(target.id) {case 'add':alert('添加');break;case 'remove':alert('删除');break;case 'move':alert('移动');break;case 'select':alert('选择');break;default :alert('业务错误');break;}}
}
用事件委托就可以只用一次dom操作就能完成所有的效果,比上面的性能肯定是要好一些的
现在讲的都是document加载完成的现有dom节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?也就是说,一个新员工来了,他能收到快递吗?
<input type="button" name="" id="btn" value="添加" />
<ul id="ul1"><li>111</li><li>222</li><li>333</li><li>444</li>
</ul>
现在是移入li,li变红,移出li,li变白,这么一个效果,然后点击按钮,可以向ul中添加一个li子节点
window.onload = function(){var oBtn = document.getElementById("btn");var oUl = document.getElementById("ul");var aLi = oUl.getElementsByTagName("li");var num = 4;// 鼠标移入变红,移出变白for(var i = 0; i < aLi.length; i++){aLi[i].onmouseover = function(){this.style.background = 'red';}aLi[i].onmouseout = function(){this.style.background = '#fff';}}// 新增节点oBtn.onclick = function(){num++;var oLi = document.createElement('li');oLi.innerHTML = 111 * num;oUl.appendChild(oLi);}
}
这是一般的做法,但是你会发现,新增的li是没有事件的,说明添加子节点的时候,事件没有一起添加进去,这不是我们想要的结果,那怎么做呢?一般的解决方案会是这样,将for循环用一个函数包起来,命名为mHover,如下:
// 将鼠标移出移入包装为一个函数
window.onload = function(){var oBtn = document.getElementById("btn");var oUl = document.getElementById("ul");var aLi = oUl.getElementsByTagName("li");var num = 4;// 鼠标移入变红,移出变白function mHover(){for(var i = 0; i < aLi.length; i++){aLi[i].onmouseover = function(){this.style.background = 'red';}aLi[i].onmouseout = function(){this.style.background = '#fff';}}}mHover();// 新增节点oBtn.onclick = function(){num++;var oLi = document.createElement('li');oLi.innerHTML = 111 * num;oUl.appendChild(oLi);mHover();}
}
虽然功能实现了,看着还挺好,但实际上无疑是又增加了一个dom操作,在优化性能方面是不可取的,那么有事件委托的方式,能做到优化吗?
window.onload = function(){var oBtn = document.getElementById("btn");var oUl = document.getElementById("ul");var aLi = oUl.getElementsByTagName("li");var num = 4;// 事件委托 鼠标移入变红,移出变白// 添加的子元素也有事件oUl.onmouseover = function(){var ev = ev || window.event;var target = ev.target || ev.srcElement;if(target.nodeName.toLowerCase() == 'li'){target.style.background = 'red';}} oUl.onmouseout = function(){var ev = ev || window.event;var target = ev.target || ev.srcElement;if(target.nodeName.toLowerCase() == 'li'){target.style.background = '#fff';}}// 新增节点oBtn.onclick = function(){num++;var oLi = document.createElement('li');oLi.innerHTML = 111 * num;oUl.appendChild(oLi);}
}
另外一个思考的问题
现在给一个场景 ul > li > div > p,div占满li,p占
前端面试题目汇总摘录(JS 基础篇 —— 2018.11.01更新)相关推荐
- 前端面试题目汇总摘录(JS 基础篇)
温故而知新,保持空杯心态 JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string type ...
- 前端面试题汇总(JS 基础篇)
前端面试题汇总(JS 基础篇)** 1.javascript 的 typeof 返回哪些数据类型** object number function boolean underfind stringty ...
- 初中级前端面试题目汇总和答案解析
笔者虽然曾今也面试过一些前端求职者,但是对于前端的笔试和面试,我觉得并不能体现一个人的真实能力,所以建议大家多修炼前端真正的技术.对于前端面试题,由于之前承诺过会出一期,所以笔者大致总结一下曾今面试的 ...
- 网上IC笔试面试题目与秋招进度(2022.11.5更新)
2022.11.5更新:(IC笔试题目有JL科技.TR半导体.HZW.MX半导体.RSKX)欢迎大家一起讨论题目,也请多多指教弟弟.笔试已经转移到新开帖子了. 目前投递60+,人才库8,offer6拒 ...
- [转]2016年Web前端面试题目汇总
记得去年 12月份刚来北京面试前端的时候 ,下面的这问题大部分有涉及到...是不是要做题的都会出这些.. HTML/CSS部分 1.什么是盒子模型? 在网页中,一个元素占有空间的大小由几个部分构成 ...
- 前端工程师必须知道的vue前端面试题目汇总
①:说说Vue和Angular.ReactJS的相同点和不同点 ②:简单描述一下Vue中的MVVM模型 ③:v-if和v-show指令有什么区别? ④:如何阻止Vue中的绑定事件不发生冒泡 ⑤:父.子 ...
- ie9无法获取未定义或 null 引用的属性“indexof”_前端JS基础篇(二)JS基本数据类型和引用数据类型及检测数据类型方法...
JS中的数据类型 (一).基本数据类型(值类型) 1.number:数字 -12.12.5.-12.5 0这些数字都是number: js中增加了一个number类型的数据:'NaN' typeof ...
- 1.面试题目汇总-嵌入式篇
1.面试题目汇总-嵌入式篇 https://blog.csdn.net/qq_33443989/article/details/76938304 2017年08月08日 20:18:10 Fogost ...
- 2018年中高级前端面试题目小结
2018年中高级前端面试题目小结 前言 关于前端面试,及面试题目,我之前有很多文章总结过,可以在右侧搜索面试,进行查找.其实面试中可以问的问题很多,最近几年,我也面试过很多工作2-4年的前端,我一般会 ...
最新文章
- Python 编写用户登录接口
- 又一篇硕士论文火了!高校硕士论文研究“董明珠自恋及其经济后果”
- Asp.Net读写XML简单方法
- linux xmind无法运行,linux安装xmind的方法步骤
- mysql5.6 pid_MySQL5.6启动报错The server quit without updating PID file
- 随想录(canvas双缓存下的性能分析)
- 【JVM】JVM的生命周期
- Unity项目中的资源管理
- MATLAB强化学习实战(三) 使用并行计算训练DQN智能体进行车道保持辅助(LKA)
- 计算机电子表格考点,计算机一级考试历年常见考点总结:Excel电子表格
- python-小米-句子反转
- 内存占用率过高怎么办 一分钟解决
- 多旋翼无人机ROSC++开发例程(一):环境配置
- 盈高入网规范管理平台linux,入网引导测试和修复测试
- 小蓝本 第一本 《因式分解技巧》 第八章 多项式的一次因式 笔记 (第八天)
- 区分LPCTSTR和LPTSTR和char *
- jQuery框架的介绍以及基本用法--操作dom
- kali新手入门教学(13)--nc连接
- linux dc退出命令,Linux dc 命令 command not found dc 命令详解 dc 命令未找到 dc 命令安装 - CommandNotFound ⚡️ 坑否...
- 机器学习面试宝典200题!