JS常见面试题总结

1.包装类对象

var s = '123'
s.age = 27
console.log(s.age) => undefined
1
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&gt;');</script>
2.进行url跳转时可以整体使用encodeURI
Location.href=encodeURI("http://cang.baidu.com/do/s?word=百度&ct=21");
3.escape用于搜索历史纪录
1
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;
}
1
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;
}
1
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  然而原型链上的属性仍然可以访问到
1
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;
}
1
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
};
1
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
}
1
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;
}
1
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));
1
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;
}
1
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;
}
1
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);
	}
}
1
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'}]
1

如果我们要获取所有的 name 值,我们可以这样做:

var name = person.map(function (item) {
    return item.name;
})
1
2
3

不过如果我们有 curry 函数:

var prop = curry(function (key, obj) {
    return obj[key]
});

var name = person.map(prop('name'))
1
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);
1
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())();
1
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]
1
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;
}
1
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;
}
1
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
1
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__
    }
}
1
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]
}
1
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]';
  };
1
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是否为1isElement = 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;
}
1
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}))
1
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.parseeval实现容易产生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 + ")");
}
1
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);
1
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的属性,相比寄生组合式继承,ES6class 多了一个 Object.setPrototypeOf(Child, Parent) 的步骤。
Promise:通过then实现链式调用,只有两种状态resolve/reject,解决异步循环嵌套问题
1
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;
1
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方法,区别主要是前者是异步加载,后者是同步加载。
1
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(() => {
	
})
1
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)
}()
1
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">
1
2
更改默认视口宽度

移动端浏览器默认视口宽度一般比设备浏览器视窗宽度大,通常是980px,我们可以通过如下标签设置视口宽度为设备宽度。

<meta name="viewport" content="width=device-width">
1

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);
}
1
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>
1
2
3
4
5

那么这句JS加在事件处理方法中

$("#datePicker").focus(function(){
      document.activeElement.blur();
});
1
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 系列的区别
  1. querySelectorAll 属于 W3C 中 Selectors API 规范, 而 getElementsBy 系列则属于 W3C DOM 规范。
  2. querySelectorAll 方法接受参数是 CSS 选择符,当传入的是不符合 CSS 选择符规范时会抛出异常,而 getElementsBy 系列则接受的参数是单一的 className,tagName 等等。
  3. 从返回值角度来看,querySelectorAll 返回的是不变的结点列表,而 getElementsBy 系列返回的是动态的结点列表。
  4. 普遍认为:getElementsBy 系列性能比 querySelectorAll 好
  5. querySelectorAll 返回值为一个 NodeList,而 getElementsBy 系列返回值为一个 HTMLCollection
  6. HTMLCollection 是元素集合而 NodeList 是节点集合(即可以包含元素,文本节点,以及注释等等)。
  7. 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()方法