18~19年大厂高级前端面招汇总之基础篇(二)

6. 怎么实现对象的深拷贝和浅拷贝?

对象属于引用类型,保存在堆内存中。浅拷贝是指简单的赋值,实际上只是复制该对象指向的指针地址而已,两个变量最终都指向同一个对象。因此一个对象改变时,另一个对象也会跟着改变。

var obj = {
	name: 'John',
	age: 30
}
var obj2 = obj;
obj2.sex = 'man';
console.log(obj); //Object {name: "John", age: 30, sex: 'man'}
console.log(obj2); //Object {name: "John", age: 30, sex: 'man'}

深拷贝是指创建一个新的内存块存储新对象,并且新对象值与被拷贝对象一样,两个对象是相互独立没有关联的。

数组对象的深拷贝可以使用slice()函数:

var arr = ['a', 'b', 'c'];
var arrCopy = arr.slice();
arrCopy[0] = 'test'
console.log(arr); // ["a", "b", "c"]
console.log(arrCopy); // ["test", "b", "c"]

带属性对象的深拷贝可以使用遍历源对象的属性(for key in object)并赋给新对象的属性的方法:

var obj = {
    name: 'John',
    age: 30
}
var deepCopy = function (source) {
    var result = {};            
    for(var key in source) {                
        if(typeof source[key] === 'object') {
            result[key] = deepCopy(source[key])
        } else {
            result[key] = source[key]
        }
    }            
    return result;
}

var objCopy = deepCopy(obj)
obj.name = 'Lee';
console.log(obj);//Object {name: "John", age: 30}
console.log(objCopy);//Object {name: "Lee", age: 30}

7. 文件上传如何做断点续传?

由于篇幅问题,这里只简单介绍基本原理:断点续传实现的基本思想就是在发送端(也称客户端)将要传输的文件分割为大小相当的多块,将这些块同时向目标服务器端发送;在服务器端的监听数据传输请求,每完成一次请求就告诉客户端可以继续发送直至全部发送完成。发送给到服务端的是N个Blob格式的二进制文件。如果需要还原整个文件只需要把这些二进制文件合并即可。

首先通过input标签可以获取到一个File格式的上传文件,如

<input type="file" multiple="multiple" />

断点核心:将File格式文件切分成多块,如每块切分为1000字节,可以使用:

// 首次上传
range = file.slice(0,1000); // range为Blob格式
upload(range)
...
// 首次上传完成后再次上传
range = file.slice(1000,2000);
upload(range)
...
// 直至全部完成

8. 防抖和节流的区别是什么?

防抖是指高频触发事件时,程序会等待,直至停止触发并经过一定时间后(如果等待期间触发将会重新计算时间)才会执行代码一次。如以下代码,无论如何频繁点击,都不会执行todo函数,直至停止点击2000毫秒内不再点击,才会触发一次todo函数。

<body>
	<div onclick="clickHandle()">防抖</div>
</body>

<script>
	function clickHandle() {
	    debounce(); // 防抖
	}

	function todo() {
	    console.log('todo!');
	}

	var debounce = (function(fn) {
	    var timeout = null;
	    return function() {
		    clearTimeout(timeout);
		    timeout = setTimeout(() => {
			    fn.apply(this, arguments); // 等效于 fn()
		    }, 2000);
	    };
	})(todo);
</script>

节流是指高频事件触发时,都会在 n 秒内执行一次,节流会稀释函数的执行频率。如以下代码,无论如何频繁点击,都会在2000毫秒后执行一次todo函数。

<body>
	<div onclick="clickHandle()">节流</div>
</body>

<script>
	function clickHandle() {
	    throttle(); // 节流
	}

	function todo() {
	    console.log('todo!');
	}

	var throttle = (function(fn) {
	    let canRun = true;
	    return function() {
		    if (!canRun) return;
		    canRun = false;
		    timeout = setTimeout(() => {
			    fn.apply(this, arguments); // 等效于 fn()
			    canRun = true;
		    }, 2000);
	    };
	})(todo);
</script>

9. 介绍浏览器事件流?

浏览器事件流包括冒泡阶段、处于目标阶段和捕获阶段。其流向如下图所示:

18~19年大厂高级前端面招汇总之基础篇(二)

可以通过addEventListener和removeEventListener来添加和删除事件监听,这两个函数接收三个参数:1.事件类型 2.事件处理函数 3. boolean值,为true时表示捕获事件,默认为false冒泡事件。如果想要中途停止冒泡或捕获,可以使用e.stopPropagation();

<body>
	<div id="container">
		<div id="child"></div>
	</div>
</body>

<script>
	/************停止捕获写法******************/
	document.getElementById('container').addEventListener('click', function(e) {
		e.stopPropagation();
		console.log('container');
	}, true)

	document.getElementById('child').addEventListener('click', function(e) {
		console.log('child');
	})
	
	/************停止冒泡写法******************/
	document.getElementById('container').addEventListener('click', function() {
		console.log('container');
	})

	document.getElementById('child').addEventListener('click', function(e) {
		e.stopPropagation();
		console.log('child');
	})
</script>

10. 介绍this各种情况?

this在使用ES5开发很常见,this并不代表函数自身,它的指向要依据调用它的对象而定,它是在执行过程中绑定的this的使用有四种情况:

(1)函数调用:this指向window全局变量,但如果函数内有定义this值,则函数内定义的值有限。

// 函数调用
function test() {      
    console.log('test:',this.a);  
}  
var a = 0;
test(); // 0

// 函数内有定义this值,则优先级更高
function test() {
    this.a = 1;      
    console.log('test:',this.a);  
}  
var a = 0;
test(); // 1

(2)对象调用:this指向调用它的对象。

function test() {  
    console.log(this.a);
}

var a = 0;
var obj = {};
obj.a = 1;
obj.m = test;
obj.m(); // 1,如果obj对象没有a属性,则为undefined

// 如果obj被其他对象引用,相当于obj2.m指针还是指向obj的堆内存
// obj对象是自己创建的,占有自己的内存块,即最终由obj调用test函数
var obj2 = {};
obj2.a = 2;
obj2.m = obj;
obj2.m.m(); // 1

// 如果obj.m被其他对象引用,由于obj.m并不是自己的函数,而是指向test函数对象,相当于obj3.m指针也指向了test函数对象
// obj.m并不是自己创建的,它指向的是test函数的内存块,同理obj3.m也是指向test函数的内存块,即最终由obj3调用test函数
var obj3 = {};
obj3.a = 3;
obj3.m = obj.m;
obj3.m(); // 3

(3)构造函数调用:this指向于只new出来的对象,与外界无关。

var a = 2; 
function test() {   
	this.a = 1;
	this.m = function(){
		console.log(this.a);
	} 
} 

var obj = new test();
console.log(obj.a); // 1
obj.m(); // 1

// 如果构造函数没有属性a,则为undefined
var a = 2; 
function test() {   
	this.m = function(){
		console.log(this.a);
	} 
} 

var obj = new test();
console.log(obj.a); // undefined
obj.m(); // undefined

(4)apply 调用 ,apply方法作用是改变函数的调用对象,此方法的第一个参数为改变后调用这个函数的对象,this指代第一个参数

var x = 0;  
function test() {    
	console.log(this.x);  
}  
var o = {};  
o.x = 1;   
test.apply(); //0
//apply()的参数为空时,默认调用全局对象。因此,这时的运行结果为0,证明this指的是全局对象。如果把最后一行代码修改为
test.apply(o); //1

箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。this就指向取决于调用该箭头函数往上一层的作用域,即看外层是否有函数,如果有,外层函数的this就是内部箭头函数的this; 如果没有,就是window。

// 箭头函数外层没有其他函数,this指向window
var foo = () => {
	console.log(this.a);
}

var a = 2;
var obj = {
	a: 3,
	foo: foo
};
obj.foo(); //2
foo.apply(obj); //2 apply在箭头函数不会生效

// 箭头函数外层有其他函数,this指向最近一层带this属性的作用域,直至冒泡到最用引用的对象
function foo() {
	this.a = 0;
	return () => {
		this.a = 1;
		return() => {
			console.log(this.a);
		}
	}
}

var a = 2;
var obj = {
	a: 3,
	foo: foo
};
obj.foo()()(); // 1。
// 如果把this.a = 1删除,则结果为0;
// 如果再把this.a = 0删除,则结果为3。this会往上冒泡取值直至调用它的对象。
// 如果再把a:3也删掉,则为undefined,因为这里引用它的是obj而不是window