高级前端软件工程师知识整理之基础篇(四)

本文深入讲解前端开发中的关键概念,包括JavaScript的垃圾回收机制、前后端联调技巧、RESTful API设计、DOM操作细节、响应式布局策略、异步执行模型、函数特性比较以及HTTP协议解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

21. JS里垃圾回收机制是什么?常用方法有哪些以及如何优化垃圾回收?

由于字符串、对象和数组没有固定大小,所以当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。JavaScript有自己的一套垃圾回收机制(Garbage Collection),解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。例如:

var a = "before";
var b = "override a";
var a = b; //重写a

这段代码运行之后,“before”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。

JavaScript垃圾回收机制常用的方法有两种:标记清除和引用计数。

(1)标记清除

这是javascript中最常用的垃圾回收方式。当执行流进入环境时,环境中的变量被标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要没直接结束,就可能会用到它们。当执行流离开环境时,则将环境中的变量标记为“离开环境”。

示例:

function test() {
	var a = 1; //被标记 ,进入环境 
	var b = 2; //被标记 ,进入环境 
}
test(); //执行完毕 之后 a、b又被标离开环境,被回收。

原理:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会移除执行流所在的环境中的变量以及被该环境中的变量引用(如闭包就是个典型的例子)的变量。之后其它变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

(2)引用计数

另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型对象赋值给该变量时,则该引用类型对象的引用次数就是1。相反,如果该声明的变量不再引用,则该引用类型对象的引用次数就减1。当引用次数变成0时,则说明没有再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。如

function test() {
	var a = {}; //a的引用次数为0 
	var b = a; //a的引用次数加1,为1 
	var c = a; //a的引用次数再加1,为2 
	var b = null; //a的引用次数减1,为1 
}

但这种回收策略有种弊端,当有变量相互赋值引用的情况,这类变量将永远不会被回收。

优化垃圾回收的策略按变量类型可以分为三种:

(1)object变量类型,如var obj = {....}

  • 在不使用该object时,把obj设为空,如obj = null;
  • 定义一个全局对象object,其他所有的object都是该全局对象的属性值,如redux和vuex中的状态值state
  •  循环使用object对象而不重新创建,把object的属性值都清空重新填充使用,算法如下:
// 删除obj对象的所有属性,高效的将obj转化为一个崭新的对象!
cr.wipe = function (obj) {
    for (var p in obj) {
         if (obj.hasOwnProperty(p))
            delete obj[p];
    }
};   

(2)数组变量类型,如var arr = [...]

  • 数组也是对象的一种,同样可以使用arr = null;
  • 数组重用,将arr.length = 0,然后重复利用

(3)函数变量类型,如var fun = function(){...}

在一些高频执行的函数中,尽量少使用闭包返回函数的写法,如在游戏的主循环,setTimeout或requestAnimationFrame来调用一个成员方法是很常见的:

setTimeout(
    (function(self) {                    
      return function () {
              self.tick();
    };
})(this), 16)

每过16毫秒调用一次this.tick(),嗯,乍一看似乎没什么问题,但是仔细一琢磨,每一次调用都返回了一个新的方法对象,这就导致了大量的方法对象垃圾!为了解决这个问题,可以将作为返回值的方法保存起来,例如:

// at startup
this.tickFunc = (
    function(self) {
      return function() {
                self.tick();
      };
    }
)(this);

// in the tick() function
setTimeout(this.tickFunc, 16);

22. 前端和后端怎么联调?

这种问题没有标准答案。这里分享一下本人曾经在一家国内上市公司任职高级前端时的前后端联调方法。产品上线过程一般会分为四个阶段,分别是开发、测试、预发布和发布。前后端联调主要发生在开发阶段。在这个阶段,前端在自己的机器里完成开发,然后连接某个固定IP的服务器联调,最终还是在自己机器的浏览器中测试联调效果。由于同源策略,这种情况浏览器一般会报出跨域错误,联调失败。目前,最常用的开发框架是vue和react,这类框架都会自带服务代理(proxy),所以要解决跨域问题实现联调只需要配置好服务代理即可。有人说我们既不使用vue也不使用react怎么办?那可以使用nodejs开发一个简单的数据转接代理服务,配合nginx在本机就可以实现服务代理。

23. 介绍RESTful及RESTful常用的Method?

这题主要考前后端通信知识,更深入的可能包括HTTP协议、通信请求的实现(如Fetch的使用)等。

RESTful是一种前后端通信的架构风格,使用这种风格开发的后台服务端被称为RESTful Web Service (又称 RESTful Web API) ,即使用 HTTP 并符合 REST 原则的 Web 服务。站在前端的角度,网络请求无外乎使用ajax、axios和fetch,它们都有一个相同的参数Method,该参数就是用来表示请求的表征状态,最常用的有5种:GET/POST/PUT/PATCH/DELETE。这里要注意,PATCH是后面加入的,表示局部更新;而PUT表示全部更新。

在使用axios请求时,GET请求传递参数使用的是params,而POST/PUT/PATCH/DELETE传递参数使用的是data,格式为json对象,要注意这点区别,如:

// GET请求
var query = {};
axios({
	method: 'get',
	url,
	params: { ...query}
}).then((res) => {
	if(res.status >= 200 && res.status < 300) {
		return res.data
	}
	return Promise.reject(res)
}, (err) => {
	return Promise.reject(err.message || err.data);
});

// POST/PUT/PATCH/DELETE请求
var body = {};
axios({
	method: 'post', // post/put/patch/delete
	url,
	data: { ...body}
}).then((res) => {
	if(res.status >= 200 && res.status < 300) {
		return res.data
	}
	return Promise.reject(res);
}, (err) => {
	return Promise.reject(err.message || err.data);
});

其中,headers使用默认值即可,也可以手动设置:

axios({
    ...
    headers:{
        'Content-Type':'application/json', // 传输的数据格式
        'Accept': 'application/json' // 想要服务器返回给我的数据格式
    }
    ...
})

在使用fetch请求时,GET请求传递参数加到url路径后面即可,而POST/PUT/PATCH/DELETE传递参数使用的是body。要注意与axios的区别,它接收的参数格式常用的有两种:可以是JSON.stringify()转换后的字符串,也可以是FormData格式(个人建议),但不能是对象{}格式。如:

_getJson = () => {
	fetch('http://www.helloui.net/api/json/products.json')
		.then((resp) => resp.json())
		.then((json) => {
			Alert.alert('请求成功:', JSON.stringify(json)); // json:Array(3) [Object, Object, Object]
		})
		.catch((error) => {
			console.log(error);
		});
}

// get请求-带参
_getWithParam = () => {
	fetch('http://www.helloui.net/rn_get?name=admin')
		.then((resp) => resp.json())
		.then((json) => {
			Alert.alert('请求成功:', JSON.stringify(json)); // json:Object {name: "admin"}
		})
		.catch((error) => {
			console.log(error);
		});
}

// post请求
_post = () => {
	let data = JSON.stringify({
		name: 'admin'
	});
	const body = new FormData();
	body.append('data', data);
	fetch('http://www.helloui.net/rn_post', {
			method: 'POST',  // 或PUT/PATCH/DELETE
			body, // 或 为JSON字符串时,改为 body:JSON.stringify(object)
			headers: {
				'Content-Type': 'multipart/form-data', // 或 为JSON字符串时,改为'Content-Type': 'application/json;charset=UTF-8',
				'Accept': 'application/json'
			}
		})
		.then((resp) => resp.json())
		.then((json) => {
			Alert.alert('请求成功:', JSON.stringify(json)); // json:Object {name: "admin"}
		})
		.catch((error) => {
			console.log(error);
		});
}

24. constructor是什么?

constructor为函数实例的一个属性值,表示创建该实例的函数构造体,如

function Employee(name, job, born) {
	this.name = name;
	this.job = job;
	this.born = born;
}
var bill = new Employee("Bill Gates", "Engineer", 1985); // bill函数实例
console.log(bill.constructor);

// 打印结果:
ƒ Employee(name, job, born) {
	this.name = name;
	this.job = job;
	this.born = born;
}

25. px、em和rem的区别是什么?如何实现H5手机端的适配?

这题考的是如何实现移动端的自动适配。

  • px:物理像素,像素px是相对于显示器屏幕分辨率而言的,常用于浏览器的长度单位。
  • em:相对长度单位,默认情况下1em = 16px,但可以通过修改其父级的font-size属性从而改变大小比例。这种单位有个问题,比如把body的font-size定义为50%,一般地会是8px。那么你在body里字体大小就是1em=8px了。可当你定义了一个div,然后把字体设置成了75%,这个时候你会发现,原来他继承了body的值,现在字体更小了,变成了6px!
  • rem:相对长度单位,默认情况下1rem = 16px,它解决了em的弊端,rem只会相对html的font-size值,不会受到其它父级font-size值的影响,也是目前实现H5手机端自适应较常用的方法。

什么是自适应?如:我想要设置一个容器宽度在手机中占全屏的一半,于是设置width:Xrem,在自适应情况下,无论是何种分辨率的手机,容器都占全屏一半,这就是自适应。这意味着在不同设备,Xrem转换为分辨率可能是200px、300px甚至是800px等任何值,即转换时针对各种设备都有一个自适应的放大或缩小的比例。公式为:(1*X)rem = (16*X)px × 比例值。因此,我们只需要把比例值计算出来并赋值给http元素,就可以实现自适应。

为了便于理解,我再举个具体的例子,比如我现在有一台移动设备分辨率为980px,UI设计师给我的设计稿是750px,那我设置多少rem才可以让div占一半呢?

设备默认:1rem = 16px,得出 980/16 = 61.25rem ,即满屏时 width=61.25rem。

设计稿:为了便于计算,假设1rem = 100px,得出 750/ 100 = 7.5rem,即满屏时 width=7.5rem。

比例值fontValue = 61.25 / 7.5 = 8.167 = 816.7%,我们只需要把http的font-size: 816.7%后,把容器的width:7.5rem就是满屏了,那么半屏就是3.75rem。不知道有没有发现设计稿的一半刚好就是375px,而这里半屏设值为3.75rem,这下你明白假设为1rem=100px的用意了吧,刚好是100倍,当然当然你也可以自己随意假设,但最终的fontValue值都是一样的,这里假设成100倍是为了便于计算设计稿中的px值转为rem。

同理,如果设计稿让你在创建一个宽120px、高120px的div,根据1rem=100px得出转换得出:

div {
    width: 1.2rem
    height: 1.2rem
}

根据上面的规律,于是我们得到了一个计算http中font-size值的算法,即自适应算法如下:

<style>
	body {
		padding: 0;
		margin: 0;
	}
	
	#container {
		width: 3.75rem;
		height: 1rem;
		background-color: red;
	}
	
	#child {
		width: 490px;
		height: 131px;
		margin-top: 10px;
		background-color: yellow;
	}
</style>

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

<script>
	(function(originWidth) {  
		var currClientWidth, fontValue;  
		__resize();
		window.addEventListener('resize', __resize, false);

		function __resize() {    
			currClientWidth = document.documentElement.clientWidth;
			console.log(currClientWidth);
			// fontValue = ((currClientWidth/16)*(100/originWidth)*100).toFixed(2);
			// 换算后 
			fontValue = ((6.25 * currClientWidth / originWidth) * 100).toFixed(2);   
			document.documentElement.style.fontSize = fontValue + '%';  
		}
	})(750); // ui设计稿的宽度,一般750或640
</script>

26. 介绍栈和堆的区别?栈和堆具体怎么存储?垃圾回收时栈和堆的区别?

这题主要考点知识点是对js中基础变量和引用变量在原理及使用上的了解。

在js引擎中,它们的区别主要在于存储的变量不同,它们存储的变量分别是:

  • 栈:存储基本变量,包括Boolean、Number、String、Undefined、Null以及指向堆内存的指针。栈存储是线性有序的存储,容量小,系统分配效率高。
  • 堆:存储引用变量,如Object、Array、Function等。堆存储要先在堆内存中新分配存储区域,之后又要把指针存储到栈内存中,系统分配效率较低。

栈和堆的具体的存储过程可以用下图来表示:

栈内存中的变量一般都是已知大小或者有范围上限的,算作一种简单存储。而堆内存存储的对象类型数据对于大小一般都是未知的。比如说当我们定义一个const对象的时候,这时常量存储的其实是指针,指向存储在堆内存中的对象,栈中的指针指向是不变的,但在堆中的数据本身的大小或者属性是可变的。另外,要注意一点,用new创建的实例都是存在堆内存中。

var a = new String('123')
var b = String('123')
var c = '123'
console.log(a==b, a===b, b==c, b===c, a==c, a===c)  
// true false true true true false
console.log(typeof a)
// 'object'

代码说明,new String()创建出来的是对象,是独立存储在堆内存中的,而直接字面量赋值和工厂模式创建出来的都是字符串。

关于栈和堆的垃圾回收机制,它们的区别是:

  • 栈内存的变量用完就会被回收了。
  • 推内存的变量由于存在很多引用,所以只有当所有引用都全部被销毁之后才能回收。

27. JS为什么要区分微任务和宏任务?它们的执行机制是什么?

这一题主要考的是对JS执行机制的了解。JavaScript是单线程的,无论是何种异步方式,实际上都是通过事件循环机制实现,那既然这样,当代码到达一定复杂度时(多个异步),JavaScript就得有一定的规律分出哪块代码先执行、哪块代码后执行。这种规律就是微任务和宏任务执行机制。这里就要学会几个概念:

  • 宏任务
  • 微任务
  • 宏任务事件队列Event Queue
  • 微任务事件队列Event Queue

我们先来认识一下异步的机制,如setTime、setInterval、Promise等,有一段代码如下:

setTimeout(() => {
    console.log(1);
},3000)

sleep(10000000)

你会发现,当sleep函数执行的时间够长的时候,setTimeout中并不会在3秒后就打印1,可能是7秒、8秒,甚至更长。

实际过程:setTimeout在3秒钟后把打印1的任务丢到了事件队列Event Queue中而不是立刻执行,只要sleep函数没有执行完成,那就只好等着,直至sleep执行完成后,才开始遍历执行事件队列Event Queue中的函数,这就是异步的运行机制。setInterval也一样,会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

现在我们来认识宏任务和微任务,请看下图:

要分清宏任务和微任务,分清它们都是哪些代码类型即可:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise().then,process.nextTick,另外Promise(()=>{这块代码会立刻执行})。遇到await立刻执行,执行完成后停止继续往下,并把await下一行代码放到微任务。

它们的执行机制是:

  • 进入主线程。
  • 执行过程中,遇到宏任务时,如果同步则马上执行,如果异步则把回调函数丢到宏任务事件队列Event Queue中。遇到微任务时,如果为Promise(...)则里面的代码块立刻执行,其它异步即Premise.then则把回调函数丢到微任务事件队列Event Queue中。
任务同步异步
立刻执行丢到宏任务Event Queue
Promise(...)则里面的代码块立刻执行丢到微任务Event Queue

 

第二步执行结束后,假设得到队列如下:

宏任务Event Queue微任务Event Queue
a主线程里微任务1
b主线程里微任务2
  • 开始检查执行微任务Event Queue,如果该队列中的函数有新的宏任务或微任务,则重复第二步。注意:新的微任务依旧在本次微任务事件中遍历执行直至队列为空。

当1中发现新的宏任务c,新的微任务3,微任务1回调函数执行结束后,变成如下并继续执行本轮微任务2、3:

宏任务Event Queue微任务Event Queue
a主线程里微任务1
b新微任务3
c 
  • 微任务Event Queue全部执行结束后,从宏任务事件队列Event Queue取出第一个任务,如a,重复第二步,直至所有宏任务队列也情况,全部代码执行结束。
宏任务Event Queue微任务Event Queue
ba里微任务1
ca里微任务2

请看下面代码:

setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('then');
})

console.log('console');

它的执行过程是这样的:

1)这段代码作为宏任务,进入主线程。

2)遇到setTimeout,是宏任务诶,但是是个异步,那就先不执行了,把setTimeout中的回调函数注册后分发到宏任务Event Queue。

3)接下来遇到了Promisenew Promise则立即执行,打印'promise',然后then函数分发到微任务Event Queue。

4)遇到console.log(),立即执行。打印'console'。第一轮宏任务执行完成,开始执行第一轮微任务,看看有哪些微任务?。

5)刚才注册到微任务Event Queue中的只有then,开始执行,打印'then'。好啦,现在微任务Event Queue也没有啦,开始执行新的宏任务。

6)第二轮开始,去宏任务Event Queue里找,发现有一个刚才注册的setTimeout回调函数,执行,打印'setTimeout'。

所以最终打印顺序是:promise、console、then、setTimeout。有点怀疑?那我们再来看一个更复杂的例子:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

足够复杂了吧,它的执行过程是这样的:

1.1)整体script作为第一个宏任务进入主线程,遇到console.log,输出1。

1.2)遇到setTimeout,是个宏任务,而且是异步的,那就把回调函数分发到宏任务Event Queue中。我们暂且记为setTimeout1

1.3)遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1

1.4)遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1

1.5)又遇到了setTimeout,将其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。第一轮执行结束,现在看看宏任务Event Queue和微任务Event Queue都有哪些?

现在开始执行第一轮微任务,队列顺序分别是process1、then1,分别打印6、8。好了,现在微任务队列没了,开始第二轮宏任务,在宏任务队列中为setTimeout1,进入执行环境。

2.1)遇到console.log,输出2。

2.2)遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process2。

2.3)遇到Promisenew Promise直接执行,输出4。then被分发到微任务Event Queue中。我们记为then2。第二轮执行结束,现在看看宏任务Event Queue和微任务Event Queue都有哪些?

现在开始执行第二轮微任务,队列顺序分别是process2、then2,分别打印3、5。好了,现在微任务队列没了,开始第三轮宏任务,在宏任务队列中为setTimeout2,进入执行环境。

3.1)遇到console.log,输出9。

3.2)遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process3。

3.3)遇到Promisenew Promise直接执行,输出11。then被分发到微任务Event Queue中。我们记为then3。第三轮执行结束,现在看看宏任务Event Queue和微任务Event Queue都有哪些?

现在开始执行第三轮微任务,队列顺序分别是process3、then3,分别打印10、12。好了,现在宏任务和微任务队列都清空了,整个程序执行完毕。所以最终打印结果:1,7,6,8,2,4,3,5,9,11,10,12。

(注)在最新的node规定中,process.nextTick()在不再作为微任务性质。

官方给出的新定义是:process.nextTick() 方法将 callback 添加到下一个时间点的队列。 在 JavaScript 堆栈上的当前操作运行完成之后以及允许事件循环继续之前,此队列会被完全耗尽。我的个人可以理解是,如果遇到process.nextTick()则以此为截点停止执行截点往后排的宏任务事件队列,直至截点前面所有的事件都执行结束后,才重新开始执行宏任务事件队列。在此过程中,process.nextTick()可以看做是一个特殊的宏任务。

因此,在新的浏览器规范中,上面例子的打印结果是:1,7,8,2,4,5,6,3,9,11,12,10

再有一个async、await的例子:

async function async1() {
	console.log('async1 start')
	await async2() // 马上执行
	console.log('async1 end') // 该行被放入微任务,直至微任务中执行该行,本函数环境的代码才继续往下执行
}
async function async2() {
	console.log('async2')
}
console.log('script start')
setTimeout(function() {
	console.log('setTimeout')
}, 0)
async1();
new Promise(function(resolve) {
	console.log('promise1')
	resolve();
}).then(function() {
	console.log('promise2')
})
console.log('script end')

// script start、async1 start、async2、promise1、script end、async1 end、promise2、setTimeout

更详细资料可以参考https://juejin.im/post/59e85eebf265da430d571f89

28. 介绍箭头函数和普通函数的区别?

箭头函数和普通函数主要有三种区别:

(1)箭头函数不绑定arguments,而是通过rest方式获取参数。普通函数会绑定arguments。

var a = function(){
	console.log(arguments);
}

var b = (...rest) => {
	console.log(rest);
}

a(1,2,3); // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
b(1,2,3); // [1, 2, 3]

(2)箭头函数不绑定this,它的this指向上下文,即父级作用域的this。普通函数时在执行中绑定this,通常有四种情况,详情可查看《18~19年大厂高级前端面招汇总之基础篇(二)》

var obj = {
  a: 10,
  b: () => {
    console.log(this.a); // undefined
    console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
  },
  c: function() {
    console.log(this.a); // 10
    console.log(this); // {a: 10, b: ƒ, c: ƒ}
  },
  d: function() {
  	return ()=>{
           console.log(this.a); //10
    }
  }
}
obj.b(); 
obj.c();
obj.d()();

(3)箭头函数没有原型属性,也不能使用new创建实例。

var a = () => {
	return 1;
}

function b() {
	return 2;
}

console.log(a.prototype); // undefined
console.log(b.prototype); // {constructor: ƒ}
let aFun = new a(); // Uncaught TypeError: a is not a constructor

29. 介绍defineProperty方法,什么时候需要用到?

defineProperty方法用于创建一个对象,该对象的属性值权限可手动配置,包括属性是否可删除、是否可见、是否可读写。

先来看一段代码:

var forin = function(obj) {
	for(var i in obj) {
		console.log(i+','+obj[i]);
	}
}

// 直接定义方式创建对象
var fun1 = {
	a: 1,
	b: 2
}
forin(fun1); // a,1 b,2
delete fun1.b;
fun1.a = 3; 
forin(fun1); // a,3

// new实例方式创建对象
var Fun2 = function() {}
Fun2.prototype.a = 1;
Fun2.prototype.b = 2;

var fun2 = new Fun2();
forin(fun2); // a,1 b,2 
delete Fun2.prototype.b
fun2.a = 3;
forin(fun2); // a,3

代码显示,以上两种创建对象实例的方法,其属性都是可修改、删除的,即使定义在函数原型中,会不会忽然觉得有点可怕。比如创建一个Common()公共组件,该组件在某处被人误删某些属性,那么整个Common()公共组件的所有引用都会出错,找bug都能找到崩溃。于是,JavaScript创建了defineProperty函数,实现了重新自定义对象属性的权限,可以实现属性的不可见和不可删除,很好解决了这个问题。

Object.defineProperty(obj, prop, descriptor)

该函数接收三个参数:

  • obj,创建的对象
  • prop,需要修改权限的属性
  • descriptor,权限配置

descriptor的配置可以使用两种方法:数据描述符和状态描述符。

数据描述符:格式{configurable, enumerable, value, writable}

属性描述
configurable表示该属性能否通过delete删除,默认为false。
enumerable表示该属性是否可以枚举,即可否通过for in访问属性。默认为false。
value表示该属性的值,可以是任何有效的JS值。默认为undefined。
writable表示该属性的值是否可写,默认为false。当为true时,表示可以被赋值运算符改变。

状态描述符:格式{configurable, enumerable, get, set}

属性描述
configurable表示该属性能否通过delete删除,默认为false。
enumerable表示该属性是否可以枚举,即可否通过for in访问属性。默认为false。
get在读取属性时调用的函数,默认值为undefined。
set在写入属性时调用的函数,默认值为undefined。

两种配置方法的实现都差不多,请看实现代码:

var forin = function(obj) {
	for(var i in obj) {
		console.log(i + ',' + obj[i]);
	}
}

// 数据描述符
var fun3 = Object.create(null);
var descriptorA = {
	writable: true,
	configurable: true,
	enumerable: true,
	value: 1
}
var descriptorB = {
	writable: false,
	configurable: false,
	enumerable: false, // 不可枚举,实现for in找不到该属性
	value: 2
}
var descriptorC = {
	writable: false, // 无法写入,即赋值无效
	configurable: true,
	enumerable: true,
	value: 1
}
Object.defineProperty(fun3, 'a', descriptorA);
Object.defineProperty(fun3, 'b', descriptorB);
Object.defineProperty(fun3, 'c', descriptorC);
fun3.a = 3;
fun3.c = 3;
forin(fun3); // a,3 c,1
console.log(Object.keys(fun3)); // ["a", "c"]

// 访问器描述符
function Fun4() {
	var _a = 1;
	var _b = 2;
	var _c = 1;
	Object.defineProperty(this, 'a', {
		configurable: true,
		enumerable: true,
		get() {
			return _a;
		},
		set: function(value) {
			_a = value;
		}
	});
	Object.defineProperty(this, 'b', {
		configurable: false,
		enumerable: false,
		get() {
			return _b;
		},
		set: function(value) {
			_b = value;
		}
	});
	Object.defineProperty(this, 'c', {
		configurable: false,
		enumerable: true,
		get() {
			return _c;
		},
		set: function(value) {}
	});
}
var fun4 = new Fun4();
fun4.a = 3;
fun4.c = 3;
forin(fun4); // a,3 c,1
console.log(Object.keys(fun4)); // ["a", "c"]
delete fun4.c; // 由于configurable为false,表示删除无效。
forin(fun4); // a,3 c,1

小贴士:for in 和 Object.key在用法上还是有一些区别的,Object.keys()用于获取对象自身所有的可枚举的属性值,但不包括原型中的属性(即Funtion.proptype定义的属性),然后返回一个由属性名组成的数组。for in 则只要是对象属性都会获取得到

30. 请介绍一下HTTP协议?

这也是一道常见的面试题,推荐一篇文章《一个HTTP打趴80%面试者》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值