JS常见面试题总结
1.包装类对象
var s = '123'
s.age = 27
console.log(s.age) => undefined
2
3
var a="abc"; 这个时候我们用a.会点出很多方法出来,这些方法根本就不是值类型的,其实这些都是包装类的,当我们点一个方法出来执行的时候,是在运行的时候立马把这个a放在一个包装类的对象里面,调用的是包装类对象的方法。当我们为这个a变量添加一个属性的时候,是加到包装类中的,当我们再次访问这个属性的时候就点不出来,因为当你点的时候又会创建一个新的包装类,这个包装类根本不存在这个属性。
2.js转义编码
escape():除了 ASCII 字母、数字和特定的符号外,对传进来的字符串全部进行转义编码
document.write(escape("Visit W3School!") + "<br />")
document.write(escape("?!=()#%&"))
Visit%20W3School%21
%3F%21%3D%28%29%23%25%26
encodeURI():对整个URL进行编码,而URL的特定标识符不会被转码。
document.write(encodeURI("http://www.w3school.com.cn")+ "<br />")
document.write(encodeURI("http://www.w3school.com.cn/My first/")+ "<br />")
document.write(encodeURI(",/?:@&=+$#"))
http://www.w3school.com.cn
http://www.w3school.com.cn/My%20first/
,/?:@&=+$#
encodeURIComponent():用于对URL的组成部分进行个别编码,而不用于对整个URL进行编码。
因此,",/?:@&=+$#",这些在encodeURI()中不被编码的符号,
在encodeURIComponent()中统统会被编码
document.write(encodeURIComponent("http://www.w3school.com.cn"))
document.write(encodeURIComponent("http://www.w3school.com.cn/p 1/"))
document.write(encodeURIComponent(",/?:@&=+$#"))
http%3A%2F%2Fwww.w3school.com.cn
http%3A%2F%2Fwww.w3school.com.cn%2Fp%201%2F
%2C%2F%3F%3A%40%26%3D%2B%24%23
应用:
1.传递带&符号的网址需要使用encodeURIComponent,这样组合的url才不会被#等特殊字符截断。
<script language="javascript">
document.write('<a href="http://passport.baidu.com/?logout&aid=7&u='+
encodeURIComponent("http://cang.baidu.com/bruce42")+'">
退出</a>');</script>
2.进行url跳转时可以整体使用encodeURI
Location.href=encodeURI("http://cang.baidu.com/do/s?word=百度&ct=21");
3.escape用于搜索历史纪录
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
3. 手写apply,call,bind和new
call:
Function.prototype.mycall = function(context) {
if(context===null) context = window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn;
return result;
}
2
3
4
5
6
7
8
9
10
11
apply:
Function.prototype.myapply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
}
else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bind:
Function.prototype.mybind = function(context) {
var args = Array.prototype.slice.call(arguments,1)
var self = this
return function() {
var newargs = Array.prototype.slice.call(arguments)
self.apply(context, args.concat(newargs))
}
}
// 例子
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined bind返回的函数使用new时会导致this绑定失效
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin 然而原型链上的属性仍然可以访问到
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Function.prototype.mybind = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例
// 可以让实例获得来自绑定函数的值
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
new:
function objectFactory() {
var obj = new Object()
// 构造函数
var self = [].shift.call(arguments)
obj.__proto__ = self.prototype
var ret = self.apply(obj,arguments)
return typeof ret === 'object' ? ret : obj
};
2
3
4
5
6
7
8
4.手写数组去重,拍平,查找指定元素,乱序
数组去重:for循环,fliter,map,set,{}
function unique2(array) {
// res用来存储结果
var res = [];
for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
var current = array[i]
if (res.indexOf(current) === -1) {
res.push(current)
}
}
return res;
}
function unique5(array) {
// 未排序
// var res = array.filter(function(item, index, array) {
// return array.indexOf(item) === index
// })
// return res
// 排序
return array.concat().sort().filter(function(item, index, array) {
return !index || item !== array[index - 1]
})
}
// 使用Map这类数据结构
function unique9(array) {
const seen = new Map()
return array.filter((a) => !seen.has(a) && seen.set(a, 1))
}
// 利用Set保证数组中数据是唯一的
var unique8 = (a) => [...new Set(a)]
// 利用Object键值对来去重
var array6 = [1, 2, 1, 1, '1',{value: 1}, {value: 1}, {value: 2}];
function unique6(array) {
var obj = {};
return array.filter(function(item, index, array){
// obj[item] = true给对象赋值对应的属性,同时返回对应的数
// return obj.hasOwnProperty(item) ? false : (obj[item] = true)
// 对于1和'1'是不同的,所以需要更改,利用typeof item在前面加上类型判断
// return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
// 对于对象类型,用typeof item + item都是object[object Object]
return obj.hasOwnProperty(typeof item + JSON.stringify(item)) ? false : (obj[typeof item + JSON.stringify(item)] = true)
})
// return obj
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
数组拍平:for循环递归,reduce,拓展运算符
var arr = [1, [2, [3, 4]]];
function flatten1(arr) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten1(arr[i]))
}
else {
result.push(arr[i])
}
}
return result;
}
function flatten2(arr) {
return arr.reduce(function(prev, next){
return prev.concat(Array.isArray(next) ? flatten2(next) : next)
}, [])
}
function flatten3(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
查找指定元素:findIndex,createIndexFinder,sortedIndex
Array.prototype.createIndexFinder = function () {
let len = this.length, arr = this;
let func = arguments[0], flag = arguments[1];
if (!flag) flag = 1;
let index = flag > 0 ? 0 : len - 1;
for (; index >= 0 && index < len; index += flag) {
if (func.call(arr, arr[index])) return index;
}
return -1;
}
function isBigEnough(element) {
return element >= 15;
}
// 根据第二个参数进行判断,负值则进行倒序遍历
console.log([12, 5, 8, 130, 44].createIndexFinder(isBigEnough, 2))
Array.prototype.sortedIndex = function () {
let arr = this, target = arguments[0];
let low = 0, height = arr.length;
while (low < height) {
var mid = Math.floor((low + height) / 2);
if (target > arr[mid]) low = mid + 1;
else height = mid;
}
return mid
}
// 判断在有序数组中的位置
console.log([10, 20, 30, 40, 50].sortedIndex(25));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
乱序:插入排序,shuffle
var values = [1, 2, 3, 4, 5];
values.sort(function(){
return Math.random() - 0.5;
});
v8 在处理 sort 方法时,当目标数组长度小于 10 时,使用插入排序;反之,使用快速排序和插入排序的混合排序。所以这个方法是有问题的
// Fisher–Yates
function shuffle(a) {
var j, x, i;
for (i = a.length; i; i--) {
j = Math.floor(Math.random() * i);
x = a[i - 1];
a[i - 1] = a[j];
a[j] = x;
// ES6:[a[i - 1], a[j]] = [a[j], a[i - 1]];
}
return a;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
5.手写节流和防抖函数
节流函数:resize改变布局时,onscroll滚动添加加载下面的图片时
// 使用时间戳
// 第一次事件会立即执行,停止触发后没办法再激活事件。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date()
context = this
args = arguments
if (now - previous > wait) {
func.apply(context, args)
previous = now
}
}
}
// 使用定时器
// 第一次事件会在n秒后执行,停止触发后依然会再执行一次事件
function throttle(func, wait) {
var context, args;
var timeout;
return function() {
context = this
args = arguments
if (!timeout) {
timeout = setTimeout(function() {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
// 鼠标移入能立刻执行,停止触发的时候还能再执行一次
function throttle(func, wait) {
var timeout, context, args, result;
var previous = 0;
var later = function() {
previous = +new Date();
timeout = null;
func.apply(context, args)
};
var throttled = function() {
var now = +new Date();
//下次触发 func 剩余的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
防抖函数:keydown事件上验证用户名,输入法的联想(输入相关内容弹出选择框)
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行.
节流的原理:如果你持续触发事件,每隔一段时间,只执行一次事件。
6.手写函数柯里化
柯里化主要就是把多个参数转换为单个参数传入,只有达到指定参数个数之后才能够执行:
比如我们有这样一段数据:
var person = [{name: 'kevin'}, {name: 'daisy'}]
如果我们要获取所有的 name 值,我们可以这样做:
var name = person.map(function (item) {
return item.name;
})
2
3
不过如果我们有 curry 函数:
var prop = curry(function (key, obj) {
return obj[key]
});
var name = person.map(prop('name'))
2
3
4
5
我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些?但是要注意,prop 函数编写一次后,以后可以多次使用,而且看起来更清晰。
eg: 实现console.log(add(1)(2)(3)(4)) console.log(add(1,2,3,4))
都打印10
function curry(fn, args) {
length = fn.length;
args = args || [];
return function () {
// var arr = args, item;
// for(var i = 0; i < arguments.length; i++) {
// item = arguments[i];
// arr.push(item)
// }
arr = [...args, ...arguments];
if (arr.length < length) {
return curry.call(this, fn, arr)
} else {
return fn.apply(this, arr)
}
}
}
function Add(a,b,c,d) {
return a + b + c + d;
}
var add = curry(Add);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
7.对象的深浅拷贝,复制,遍历以及判断对象相等
深浅拷贝:JSON,递归,以及JQ的extend方法,函数库lodash
提供_.cloneDeep()
JSON.parse(JSON.stringify(arr2)) // 无法拷贝函数
var deepCopy = function(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
// 存在循环引用则跳出循环
if(obj === obj[key]) continue;
// newObj[key] = obj[key]; // 浅拷贝
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; //递归调用深拷贝
}
}
return newObj;
}
正则处理:
function getReg(a){
var c = a.lastIndexOf('/');
// 取出正则//中间的部分并转义
var reg = a.substring(1,c);
var escMap = {'"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t', '\w': '\\w', '\s': '\\s', '\d': '\\d'};
for(var i in escMap){
if(reg.indexOf(i)){
reg.replace(i,escMap[i]);
}
}
// 获取最后的全局变量标志g
var attr = a.substring(c+1);
return new RegExp(reg, attr);
}
函数处理:注意return要加空格
res[item] = new Function("return "+obj[item].toString())();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extend:标志位控制深浅拷贝,避免循环引用,待复制类型是数组和对象要分开判断
function extend() {
// 默认浅拷贝
var deep = false;
var target, arr;
var istype = Object.prototype.toString;
if (typeof arguments[0] === 'boolean') {
deep = arguments[0];
target = arguments[1];
arr = Array.prototype.slice.call(arguments, 2);
} else {
target = arguments[0];
arr = Array.prototype.slice.call(arguments, 1);
}
// 如果target不是对象,我们是无法进行复制的,所以设为 {}
if (istype.call(target) !== '[object Object]' && istype.call(target) !== '[object Function]') {
terget = {}
}
arr.forEach((item, index) => {
// 要求不能为空 避免 extend(a,,b) 这种情况
if (item !== null) {
for (var name in item) {
// 防止循环引用
if (target === item[name]) {
continue;
}
if (deep && typeof item[name] === 'object') {
// 判断复制的属性值为数组还是对象
target[name] = Array.isArray(item[name]) ? [] : {};
target[name] = extend(deep, target[name], item[name])
} else {
target[name] = item[name]
}
}
}
})
return target
}
var obj1 = {
a: 1,
b: { b1: 1, b2: 2 },
k: 2
};
var obj2 = {
b: { b1: 3, b3: 4 },
c: 3
};
var obj3 = {
d: 4,
k: [2]
}
console.log(extend(true, obj1, obj2, obj3));
a: 1
b: {b1: 3, b3: 4}
c: 3
d: 4
k: [2]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
遍历:终止循环,对象和数组的判断(9.类型判断的isArrayLike),this绑定
function each(obj, callback) {
var length, i = 0;
if (isArrayLike(obj)) {
length = obj.length;
for (; i < length; i++) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
} else {
for (i in obj) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
}
return obj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
jQuery:
$.each($("p"), function(){ $(this).hover(function(){ ... }); })
1
2
3虽然我们经常会这样写:
$("p").each(function(){ $(this).hover(function(){ ... }); })
1
2
3但是因为 $("p").each() 方法是定义在 jQuery 函数的 prototype 对象上面的,而 $.each()方法是定义 jQuery 函数上面的,调用的时候不从复杂的 jQuery 对象上调用,速度快得多。所以我们推荐使用第一种写法。
对象相等:
8.手写继承和对象创建
继承:原型继承,借用构造函数继承,组合继承,寄生组合继承
// 注意: prototype是函数才会有的属性
//原型继承
function father1() {
this.flag = true;
}
father1.prototype.flag = false;
father1.prototype.time = ["1","2"];//引用类型产生的问题
function son1() {
this.age = 21;
}
son1.prototype = new father1();
var obj1 = new son1();
obj1.time.push("3");
console.log(obj1.flag);
console.log(obj1.constructor === father1);
var obj11 = new son1();
console.log(obj11.time);//会受到obj1的影响
//借用构造函数继承--解决引用类型所造成的问题
function father2() {
this.color = ["blue","green","yellow"];
}
function son2() {
father2.apply(this);//this是对象(构造函数创建所返回的对象),而son2则是一个函数
}
var obj2 = new son2();
obj2.color.push("black");
console.log(obj2.color);
var obj21 = new son2();
console.log(obj21.color);
//组合继承
function father3(name) {
this.color = ["blue","green","yellow"];
this.name = name;
}
father3.prototype.sayName = function() {
console.log(this.name);
};
function son3(name) {
father3.call(this,name);//实例属性,可以向父类的构造函数传参 第一次调用父类的构造函数
}
son3.prototype = new father3();//第二次调用父类的构造函数
var obj3 = new son3("baotao");
console.log(obj3.name);
obj3.sayName();
var obj31 = new son3("baochen");
console.log(obj31.name);
obj31.sayName();
//原型式继承
//ES5通过Object.create()方法实现原型式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
//寄生组合继承--由于组合继承会多次调用父类的构造函数
function inheritPrototype(Father4,Son4) {
var prototype = Object.create(Father4.prototype);//原型式继承,没有调用父类的构造函数
prototype.constructor = Father4;
Son4.prototype = prototype;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
对象创建:工厂模式,构造函数,原型字面量,组合方式
//利用函数进行封装--工厂模式
function createPerson1(name,age){
var o = new Object();
o.name = name;
o.age = age;
return o;
}
console.log(createPerson1("baotao", 21));
console.log(createPerson1("baotao", 22) instanceof Object);//都是Object类型,无法辨别出不同的类型
//采用构造函数解决这个问题
function createPerson2(name,age){
this.name = name;
this.age = age;
}
var obj = new createPerson2("baotao",21);//使用new操作符,即是构造函数
console.log(obj);
console.log(obj instanceof createPerson2);//区别于工厂模式,不仅是Object,还是createPerson2类型
//由于有些属性是共用的,对此需要使用原型对象
function createPerson3(age){
//console.log(this.__proto__===createPerson3.prototype);//true
createPerson3.prototype.name = "baotao";
this.age = age;
createPerson3.prototype.sayName = function() {
console.log(this.name);
}
}
var obj1 = new createPerson3(27);
var obj2 = new createPerson3(21);
obj1.sayName();
obj2.sayName();
//Object.getPrototypeOf()获取原型对象
console.log(Object.getPrototypeOf(obj1) === createPerson3.prototype);
//delete操作符删除实例属性
obj1.name = "baochen";
console.log(obj1.name+"---"+obj2.name);
delete obj1.name;
console.log(obj1.name);
//Object.hasOwnProperty()判断属性是否在实例中
console.log(Object.hasOwnProperty(obj1.name));
// for-in返回对象的可枚举属性
// Object.keys()方法,接收一个对象,返回一个包含所有可枚举属性的数组
for(key in obj1){//in操作符后面跟实例对象
console.log(key);//age name sayName
}
var result1 = Object.keys(obj1);
console.log(result1);//age
console.log(Object.getOwnPropertyNames(createPerson3));//返回所有实例属性
//原型字面量
function Person() { this.name= "baotao"}
var obj4 = new Person();//在重写原型对象前不要创建实例对象,会有问题
Person.prototype = {
//constructor:Person 加上这句话
name:"baotao",
age:21,
sayName:function () {
console.log(this.name);
}
};
//console.log(obj4.age);--undefined
var obj3 = new Person();
console.log(Person.prototype.constructor === Person);//false 改变了constructor的指向 //采用组合方式创建对象
function Person() {this.time=["1","2"]}//在构造函数中定义引用类型
Person.prototype = {
name:"baotao",
age:21,
time:["1","2"],
sayName:function () {
console.log(this.name);
}
};
var obj1 = new Person();
var obj2 = new Person();
obj2.time.push("3");
console.log(obj1.time);//1,2
console.log(obj2.time);//1,2,3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
9.类型判断
类型判断:typeof, instanceof,Object.prototype.toString.call(),判断是window或者纯粹的对象
typeof可以判断Undefined、Boolean、Number、String、Object、Function,但是不能判断Null以及Object中的Date、RegExp、Error 等
instanceof是根据原型链进行判断的,具体实现如下:
function(left,right) {
if(typeof left !== 'object' && typeof left !== 'function') {
return false
}
let proto = left.__proto__;
let prototype = right.prototype;
while(true) {
if(proto === null) return false;
if(proto === prototype) return true;
proto = proto.__proto__
}
}
2
3
4
5
6
7
8
9
10
11
12
Object.prototype.toString
会返回一个由 "[object " 和 class 和 "]" 组成的字符串,而 class 是要判断的对象的内部属性:
var number = 1; // [object Number]
var string = '123'; // [object String]
var boolean = true; // [object Boolean]
var und = undefined; // [object Undefined]
var nul = null; // [object Null]
var obj = {a: 1} // [object Object]
var array = [1, 2, 3]; // [object Array]
var date = new Date(); // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g; // [object RegExp]
var func = function a(){}; // [object Function]
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]
function a() {
console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当然判断数组可以使用Array.isArray
,本质也是使用了Object.prototype.toString
,实现如下:
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
2
3
判断空对象,window,DOM元素节点,以及PlainObject
采用for-in遍历判断空对象:
function isEmptyObject( obj ) {
var name;
for ( name in obj ) {
return false;
}
return true;
}
利用window对象自身有个属性window指向自身:
function isWindow( obj ) {
return obj != null && obj === obj.window;
}
DOM元素节点的nodeType是否为1:
isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
};
PlainObject是只能通过 {} 和 new Object 创建的对象,或者没有原型:
proto = Object.getPrototypeOf(obj);
// 没有原型的对象是纯粹的,Object.create(null) 就在这里返回 true
if (!proto) {
return true;
}
// 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor
Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
// 在这里判断 Ctor 构造函数是不是 Object 构造函数,用于区分自定义构造函数和 Object 构造函数
return typeof Ctor === "function" && Object.prototype.hasOwnProperty.toString.call(Ctor) === Object.prototype.hasOwnProperty.toString.call(Object);
// 判断类数组对象
function isArrayLike(obj) {
// obj 必须有 length属性
var length = !!obj && "length" in obj && obj.length;
var typeRes = type(obj);
// 排除掉函数和 Window 对象
if (typeRes === "function" || isWindow(obj)) {
return false;
}
// 1.是数组 2.长度为 0 3.lengths 属性是大于 0 的数字类型,并且obj[length - 1]必须存在
return typeRes === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
10.动手实现JSON.stringfy和JSON.parse方法
JSON.stringfy:包装类转成原始类型,undefined/Function/symbol会被忽略,不可枚举的属性忽略,循环引用的属性也要忽略
function jsonStringfy(obj) {
let type = typeof obj;
if (type !== 'object' || type === null) {
if (/string|undefined|function/.test(type)) {
obj = '"' + obj + '"'
}
return String(obj)
} else {
let json = [];
arr = (obj && obj.constructor === Array);
for (let k in obj) {
let v = obj[k];
let type = typeof v;
if (/string|undefined|function/.test(type)) {
v = '"' + v + '"'
} else if(type === 'object') {
v = jsonStringfy(v)
}
json.push((arr ? "" : '"' + k + '":') + String(v))
}
return (arr ? '[' : '{') + String(json) + (arr ? ']' : '}')
}
}
console.log(jsonStringfy({x: 5}));
console.log(jsonStringfy([1, "false", false]));
console.log(jsonStringfy({b: undefined}))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
JSON.parse:eval
实现容易产生XSS,改为new Function()
也行
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
rx_one.test(
json
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
var obj = eval("(" +json + ")");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
闭包的作用
11.手写一个闭包以及闭包具有保留外层变量引用的特性:
bind实现this绑定和参数传递
setTimeout() 绑定this,或者追加第三个参数传入
回调函数...
function say(number) {
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i)
}
}
return arr[number]()
}
// 打印为10
say(3);
function say(number) {
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = (function (index) {
return function () {
console.log(index)
}
})(i)
}
return arr[number]()
}
// 打印为3
say(3);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
闭包的缺陷:闭包会导致原始作用域链不释放,造成内存泄漏(占用)
如何解决:将用完的函数或者变量置为null
JavaScript的自动垃圾回收机制:
- 标记清除(最常用)
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
- 引用计数
引用计数(reference counting)的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
12.谈谈ES6
let/const:不存在变量提升,有暂时性死区,引入块级作用域,const变量需要立刻赋值且不能改变,如果const赋值是一个对象,则只能保证指向这个对象的引用不能改变,可以改变对象的属性,需要保证内部属性不被修改,可以使用Object.freeze()冻结这个对象。
箭头函数:没有自己的this(通过查找作用域链来确定),不能new,没有arguments和原型
解构赋值,Map/Set类型
Proxy和Reflect:proxy进行对象属性访问的劫持,refelect则是把Object上语言内部的方法放置到这个对象上
模板字符串,数组拓展方法(...,includes,find,findIndex,flat)
class:通过extends实现继承,constructor为构造函数,函数和变量用static修饰相当于添加为该class的属性,相比寄生组合式继承,ES6 的 class 多了一个 Object.setPrototypeOf(Child, Parent) 的步骤。
Promise:通过then实现链式调用,只有两种状态resolve/reject,解决异步循环嵌套问题
2
3
4
5
6
7
async/await:更好的异步处理,每个await返回一个Promise实例,将函数设为async则会继发执行await,将异步请求结果用await输出,而不是在await后面直接添加异步请求方法
多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
2
3
4
5
6
7
8
9
10
11
12
13
Module模块化:Node(require/module.exports和exports)ES6(import/export):
区别 | CommonJS | es6 |
---|---|---|
加载原理 | 第一次加载模块就会执行整个模块,再次用到时,不会执行该模块,而是到缓存中取值 | 不会缓存运行结果,动态的去被加载的模块中取值,并且变量总是绑定其所在模块 |
输出 | 值的拷贝(模块中值的改变不会影响已经加载的值) | 值的引用(静态分析,动态引用,原来模块值改变会改变加载的值) |
加载方式 | 运行时加载(加载整个模块,即模块中的所有接口) | 编译时加载(只加载需要的接口) |
this指向 | 指向当前模块 | 指向undefiend |
循环加载 | 只输出已经执行的部分,还未执行的部分不会输出 | 遇到模块加载命令import时不会去执行模块,而是生成一个动态的只读引用,等到真正用到时再去模块中取值。只要引用存在,代码就能执行 |
相关用法如下:
export default 命令
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
// export-default.js
export default function () {
console.log('foo');
}
上面代码是一个模块文件export-default.js,它的默认输出是一个函数。其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
import * as circle from './circle';
因为require是运行时加载模块,import命令无法取代require的动态加载功能。
import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。
2
3
4
5
6
7
8
9
10
11
12
13
14
13.谈谈this的理解
- 在调用函数时使用 new 关键字,函数内的 this 是一个全新的对象。
- 如果apply、call 或 bind 方法用于调用、创建一个函数,函数内的 this 就是作为参数传入这些方法的对象。
- 当函数作为对象里的方法被调用时,函数内的 this 是调用该函数的对象。比如当 obj.method()被调用时,函数内的 this 将绑定到 obj 对象。
- 如果调用函数不符合上述规则,那么this 的值指向全局对象(global object)。浏览器环境下 this 的值指向 window 对象,但是在严格模式下('use strict'),this 的值为 undefined。
- 如果符合上述多个规则,则较高的规则(1号最高,4 号最低)将决定 this 的值。
- 如果该函数是ES2015 中的箭头函数,将忽略上面的所有规则,this 被设置为它被创建时的上下文
14.实现一个Sleep函数
可以使用var start = new Date; while(new Date - start <= sleepTime)
来实现,可是会造成性能负担
采用promise:
function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time))
}
sleep(3000).then(() => {
})
2
3
4
5
6
7
采用async/await:
function sleep(delay) {
return new Promise(reslove => {
setTimeout(reslove, delay)
})
}
// !表示立即执行函数
!async function test() {
const t1 = +new Date()
await sleep(3000)
const t2 = +new Date()
console.log(t2 - t1)
}()
2
3
4
5
6
7
8
9
10
11
12
15.移动端开发处理上有哪些坑
点击延迟
移动浏览器上支持的双击缩放操作,以及IOS Safari 上的双击滚动操作,是导致300ms的点击延迟主要原因。
touchstart: //手指放到屏幕上时触发 touchmove: //手指在屏幕上滑动式触发 touchend: //手指离开屏幕时触发 touchcancel: //系统取消touch事件的时候触发,这个好像比较少用 click://在这个dom(或冒泡到这个dom)上手指触摸开始,且手指未曾在屏幕上移动(某些浏览器允许移动一个非常小的位移值),且在这个在这个dom上手指离开屏幕,且触摸和离开屏幕之间的间隔时间较短(某些浏览器不检测间隔时间,也会触发click才能触发
上述事件发生顺序:在移动端,手指点击一个元素,会经过:touchstart --> touchmove -> touchend --》click。
双击缩放: 假定这么一个场景。用户在 浏览器里边点击了一个链接。由于用户可以进行双击缩放或者双击滚动的操作,当用户一次点击屏幕之后,浏览器并不能立刻判断用户是确实要打开这个链接,还是想要进行双击操作。因此,浏览器就等待 300 毫秒,以判断用户是否再次点击了屏幕。
解决方式:
禁用缩放
对于不需要缩放的页面,通过设置meta
标签禁用缩放,表明这个页面是不需要缩放的,双击缩放就没有意义了。此时浏览器可以禁用默认的双击缩放行为并且去掉300ms的点击延迟。
该方法缺点在于必须通过完全禁用缩放来达到去掉点击延迟的目的,通常情况下我们还是希望能通过双指缩放来进行缩放操作,比如放大图片,很小的一段文字。
<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">
2
更改默认视口宽度
移动端浏览器默认视口宽度一般比设备浏览器视窗宽度大,通常是980px,我们可以通过如下标签设置视口宽度为设备宽度。
<meta name="viewport" content="width=device-width">
chrome 中如果设置了上述meta标签,那浏览器就可以认为该网站已经对移动端做过了适配和优化,就无需双击缩放操作了。这个方案相比方案一的好处在于,它没有完全禁用缩放,而只是禁用了浏览器默认的双击缩放行为,但用户仍然可以通过双指缩放操作来缩放页面。
zepto的tap事件
zepto 的touch模块中自定义了tap事件,用于代替click事件,表示一个轻击操作。touch模块实现tap的原理是绑定事件touchstart,touchmove和touchend到document上,然后通过计算touch事件触发的时间差,位置差来实现了自定义的tap,swipe等。
zepto自定义的tap操作虽然可以解决300ms点击延迟问题,但存在著名的“点透”问题:
当手指触摸到屏幕的时候,会触发两个事件,一个是touch 一个是click,touch先执行,touch执行完成后,A从Dom Tree上面消失了,而且由于移动端click还有延迟200-300ms的关系,当系统要触发click的时候,发现在用户点击的位置上面,目前离用户最近的元素是B,所以就直接把click事件作用在B元素上面了.
fastclick 解决300ms延迟
基本原理:FastClick的实现原理是在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后真正的click事件阻止掉。
fastClick的核心代码
FastClick.prototype.onTouchEnd = function(event){ // 一些状态监测代码 // 从这里开始, if (!this.needsClick(targetElement)) { // 如果这不是一个需要使用原生click的元素,则屏蔽原生事件,避免触发两次click event.preventDefault(); // 触发一次模拟的click this.sendClick(targetElement, event); } }
1
2
3
4
5
6
7
8
9这里可以看到,FastClick在
touchEnd
的时候,在符合条件的情况下,主动触发了click事件,这样避免了浏览器默认的300毫秒等待判断。为了防止原生的click被触发,这里还通过event.preventDefault()
屏蔽了原生的click事件。
通过sendClick
模拟click事件:
FastClick.prototype.sendClick = function(targetElement, event) {
// 这里是一些状态检查逻辑
// 创建一个鼠标事件
clickEvent = document.createEvent('MouseEvents');
// 初始化鼠标事件为click事件
clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
// fastclick的内部变量,用来识别click事件是原生还是模拟
clickEvent.forwardedTouchEvent = true;
// 在目标元素上触发该鼠标事件,
targetElement.dispatchEvent(clickEvent);
}
2
3
4
5
6
7
8
9
10
11
就目前而言,FastClick 非常实际地解决 300 毫秒点击延迟的问题。唯一的缺点可能也就是该脚本的文件尺寸 (尽管它只有 10kb)。
对比总结
**禁用缩放:**简单,但同时也使的网页无法缩放,不适用于未对移动端浏览做适配优化的网页。
**更改默认视口宽度:**简单,但需要浏览器支持。
tap事件:能较好解决点击延迟,并且对其他移动端触摸事件也有较好支持,存在点透问题。
**fastclick:**当前较好的专门解决点击延迟的库,脚本尺寸相对较大。
移动端触摸弹出的手机键盘处理
但是在移动端,input会默认触发手机的虚拟键盘,目前有两个方案,一个是给input添加readonly属性,另一个就是在input事件处理方法前面添加一句document.activeElement.blur()
使用readonly属性
使用readonly
方式来阻止虚拟键盘弹出应该是最简单最优雅的方式了。readonly 属性规定输入字段为只读。只读字段是不能修改的。不过,用户仍然可以使用 tab 键切换到该字段,还可以选中或拷贝其文本。
值得一提的是它的取值,只要声明了readonly属性,不管取什么值都可以,比如readonly=""
、readonly="readonly"
、readonly="abc"
都是一样的
优点:简单 缺点:在iOS的Safari中无效(未做更多情况测试)
document.activeElement.blur()
document.activeElement
属性始终会引用DOM中当前获得了焦点的元素。元素获得焦点的方式有用户输入(通常是按Tab键)、在代码中调用focus()
方法和页面加载。
当你点击input的时候,document.activeElement
获得了DOM中被聚焦的元素,也就是你点击的input,而调用.blur()
方法,blur
我相信大家都知道吧,就是取消聚焦。获得被聚焦的元素然后强制blur以达到没有聚焦的样子。
优点:支持Android、iOS 缺点:需要添加额外的JS代码
这句代码加在什么地方?加入有如下HTML
<div class="calendar">
<div>
<input type="text" id="datePicker" class="date_picker" placeholder="点击选择入住日期"/>
</div>
</div>
2
3
4
5
那么这句JS加在事件处理方法中
$("#datePicker").focus(function(){
document.activeElement.blur();
});
2
3
移动端没有鼠标移入移出事件,但是有触摸事件
- touch类事件
触摸事件,有touchstart touchmove touchend touchcancel
- tap类事件
触碰事件,用于代替click事件,有tap longTap singleTap doubleTap
- swipe类事件
滑动事件,有swipe swipeLeft swipeRight swipeUp swipeDown
16.浏览器存储
- cookie在浏览器请求中每次都会附加请求头中发送给服务器。浏览器所实现的大小一般是在4KB左右
- localStorage保存数据会一直保存没有过期时间,不会随浏览器发送给服务器。大小5M或更大
- sessionStorage仅当前页面有效一旦关闭就会被释放。也不会随浏览器发送给服务器。大小5M或更大
- sessionStorage(如果是点击该页面的链接产生的新页面则共享,新打开一个页面不会共享);localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。
- Web Storage 支持事件通知机制,可以通过监听
storage
事件将数据更新的通知发送给监听者
17.DOM的常规操作
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
添加:appendChild()
移出:removeChild()
替换:replaceChild()
插入:insertBefore()
复制:cloneNode(true)
查找:
getElementsByTagName() //通过标签名称
getElementsByClassName() //通过标签名称
getElementsByName() //通过元素的Name属性的值
getElementById() //通过元素Id,唯一性
- querySelectorAll 与 getElementsBy 系列的区别
- querySelectorAll 属于 W3C 中 Selectors API 规范, 而 getElementsBy 系列则属于 W3C DOM 规范。
- querySelectorAll 方法接受参数是 CSS 选择符,当传入的是不符合 CSS 选择符规范时会抛出异常,而 getElementsBy 系列则接受的参数是单一的 className,tagName 等等。
- 从返回值角度来看,querySelectorAll 返回的是不变的结点列表,而 getElementsBy 系列返回的是动态的结点列表。
- 普遍认为:getElementsBy 系列性能比 querySelectorAll 好
- querySelectorAll 返回值为一个 NodeList,而 getElementsBy 系列返回值为一个 HTMLCollection
- HTMLCollection 是元素集合而 NodeList 是节点集合(即可以包含元素,文本节点,以及注释等等)。
- node.childNodes,querySelectorAll(虽然是静态的) 返回的是 NodeList,而 node.children 和 node.getElementsByXXX 返回 HTMLCollection。
18.for in 和for of的区别
for..of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合.但是不能遍历对象,因为没有迭代器对象.与forEach()不同的是,它可以正确响应break、continue和return语句
for-of循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用for-in循环(hasOwnPropery方法可以判断某属性是否是该对象的实例属性)或内建的Object.keys()方法