面临找工作了,准备收集一些面试题目,供自己以及大家一起学习吧~
面试题目来源于网络,作者只是整理~
一、问题一: 关于 const 和 let 声明的变量不在 window 上的疑问?
关于 const 和 let 声明的变量不在 window 上的疑问?
复制代码
回答区:
在ES5中
规定顶层对象
的属性和全局变量
是等价的。通过 var 声明的变量或者函数既是全局属性又是顶层对象的属性。
var a = 10;
var f = function(){}
console.log(window.a) // 10
console.log(window.f) // ƒ (){}
复制代码
ES6中规定,var 声明的全局函数和全局变量, 依旧是顶层对象的属性,但是通过let或者const声明的全局函数和全局变量,却不再是顶层对象的属性。
const b = 20;
let ff = function(){};
console.log(window.b) // undefined
console.log(window.ff) // undefined
复制代码
[图片上传中...(image-4a7475-1554735779699-25)]
<figcaption></figcaption>
通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中
const b = 20;
let ff = function(){};
console.log(b)
console.log(ff)
复制代码
二、问题二: ['1', '2', '3'].map(parseInt)
['1', '2', '3'].map(parseInt) 解析
复制代码
回答区
map函数的格式:
array.map( function( currentValue, index, arr ), thisValue )
第一个参数: 必须。函数,数组中的每个元素都会执行这个函数
currentValue 必须。当前元素的值
index 可选。当前元素的索引值
arr 可选。当前元素属于的数组对象
第二个参数: 可选。对象作为该执行回调时使用,传递给函数,用作 "this" 的值。
如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。
复制代码
上题可以理解为执行下面的三条:
parseInt('1', 0) //radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理。这个时候返回1
parseInt('2', 1) //基数为1(1进制)表示的数中,最大值小于2,所以无法解析,返回NaN
parseInt('3', 2) //基数为2(2进制)表示的数中,最大值小于3,所以无法解析,返回NaN
复制代码
parseInt()详解
主要解释下面的计算方法:
parseInt("10"); //返回 10
parseInt("19",10); //返回 19 (10+9)
parseInt("11",2); //返回 3 (2+1)
parseInt("17",8); //返回 15 (8+7)
parseInt("1f",16); //返回 31 (16+15)
parseInt("010"); //未定:返回 10 或 8
复制代码
parseInt(string,radix);
复制代码
规则如下:
其中的基数 radix.(不代表着进制) 很多人都误以为它代表着要转换的进制数。
string要转换的字符串,string 以 "0x" 开头,parseInt() 会把 string 的其余部分解析为十六进制的整数。
如果 string 以 0 开头,那么会把其后的字符解析为八进制或十六进制的数字。如果 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数。
复制代码
知道上面的规则后:
parseInt("10");
// 默认radix为10,string为数字开头,则解析为10进制的整数,则parseInt("10")=1*10^1+0*10^0=10;不变,其中10为基数]
parseInt('11',2);
// radix 为2, string为数字开头,则 parseInt('11',2) =1*2^1+1*2^0=3; 其中2为基数
parseInt('1f',16);
// string为1f,解析为16进制。radix为16,则=1*16^1+15*16^0=31;其中16为基数,f=15;
parseInt("17",6)=1,parseInt('17',9)=16;
//当解析17时,1属于6进制范围,7不属于6进制范围,当string的数字小于radix时(7<6),它会只解析到它的上一位,
// parseInt('17',6) = parseInt('1',6) = 1;
复制代码
例题:
var a=["1", "2", "3", "4", "5"]; a.map(parseInt);
parseInt('1',0) // 1
parseInt('2',1) // nan
parseInt('3',2) // 因为2进制范围为(0-2) 3不在2进制范围,所以NaN
parseInt('4',3) // 因为3进制范围为(0-2) 4不在3进制范围,所以NaN
parseInt('5',4) // NaN
复制代码
三、问题三: 说说你对 Set、Map、WeakSet 和 WeakMap 的认识
说说你对 Set、Map、WeakSet 和 WeakMap 的认识
复制代码
Set
Set可以理解成是一个数据结构,是一种集合类型的数据结构,类似与数组。内部的成员都是唯一的,不重复,无序。
new Set([iterable])
复制代码
举例子认识一下:
var s = new Set();
[1,2,3,4,5,4,3,2].forEach((x) => {
s.add(x)
})
// 数组去重
var arr = [1,2,3,4,5, 4,5,6,7,1];
[...new Set(arr)] // [1, 2, 3, 4, 5, 6, 7]
复制代码
向set中添加内容的时候不会进行类型转换,比如 5 和 “5” 会存为两个值。其内部使用了类似与===
的机制,但是由不一样,因为 NaN === NaN
返回false,但是set中却会只存一次,认为是相同的。
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1]) // [5, "5"]
复制代码
Set的属性和方法
(1)Set的属性
constructor
: 构造函数size
:元素数量
let set = new Set([1, 2, 3, 2, 1])
console.log(set.length) // undefined
console.log(set.size) // 3
复制代码
(2)Set的实例方法
add(value)
:新增,相当于 array里的pushdelete(value)
:存在即删除集合中valuehas(value)
:判断集合中是否存在 valueclear()
:清空集合
set的基本操作:
let set = new Set()
set.add(1).add(2).add(1)
set.has(1) // true
set.has(3) // false
set.delete(1)
set.has(1) // false
复制代码
Array.from
方法可以将 Set
结构转为数组
var set = new Set([1,2,3,4,5]);
var array = Array.form(set);
console.log(array);
var arr = [...set];
复制代码
最重要的就是遍历方法了:
keys()
:返回一个包含集合中所有键的迭代器values()
:返回一个包含集合中所有值得迭代器entries()
:返回一个包含Set对象中所有元素得键值对迭代器forEach(callbackFn, thisArg)
:用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值
var set = new Set([1,2,3,4]);
console.log(set.keys());
for (let i of set.keys()) {
console.log(i);
}
// SetIterator {1, 2, 3, 4}
// 1
// 2
// 3
// 4
for (let i of set.values()) {
console.log(i);
}
// 1
// 2
// 3
// 4
for (let i of set.entries()) {
console.log(i);
}
// 返回键值对
// [1, 1]
// [2, 2]
// [3, 3]
// [4, 4]
set.forEach((value, key) => {
console.log(key + ' : ' + value)
})
// 1 : 1
// 2 : 2
// 3 : 3
// 4 : 4
复制代码
set默认使用的是values之后的迭代器。
- 使用map、filter
var set = new Set([1,2,3,4]);
[...set].map((item) => {
return item * 2;
})
// 2,4,6,8
[...set].filter((item) => {
return item >= 4;
})
// 4
复制代码
因此,Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference)
let set1 = new Set([1, 2, 3])
let set2 = new Set([4, 3, 2])
let intersect = new Set([...set1].filter(value => set2.has(value)))
let union = new Set([...set1, ...set2])
let difference = new Set([...set1].filter(value => !set2.has(value)))
console.log(intersect) // Set {2, 3}
console.log(union) // Set {1, 2, 3, 4}
console.log(difference) // Set {1}
复制代码
WeakSet
WeakSet允许你存放弱引用的对象。
WeakSet 与 Set的区别:
1、WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
2、WeakSet 存放的都是弱引用的对象,垃圾回收机制不会因为这些弱引用而放弃对该对象的回收。
3、WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素
复制代码
var set = new WeakSet([[1,2], [3,4]]);
console.log(set)
复制代码
[图片上传中...(image-d45462-1554735779698-24)]
<figcaption></figcaption>
add(value)
:在WeakSet 对象中添加一个元素valuehas(value)
:判断 WeakSet 对象中是否包含valuedelete(value)
:删除元素 value
var ws = new WeakSet()
var obj = {}
var foo = {}
ws.add(window)
ws.add(obj)
ws.has(window) // true
ws.has(foo) // false
ws.delete(window) // true
ws.has(window) // false
复制代码
Map
Map是一种数据结构,存储键值对。
Map构造函数的参数
var map = new Map([['a', 1], ['b', 2]]);
// Map(2) {"a" => 1, "b" => 2}
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
// Map(2) {"foo" => 1, "bar" => 2}
复制代码
任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数。
let map = new Map();
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
复制代码
Map 的属性及方法
(1)属性
- constructor
- size
const map = new Map([
['name', 'An'],
['des', 'JS']
]);
map.size // 2
复制代码
(2)方法
set(key, value)
:向字典中添加新元素get(key)
:通过键查找特定的数值并返回has(key)
:判断字典中是否存在键keydelete(key)
:通过键 key 从字典中移除对应的数据clear()
:将这个字典中的所有元素删除
遍历方法
Keys()
:将字典中包含的所有键名以迭代器形式返回values()
:将字典中包含的所有数值以迭代器形式返回entries()
:返回所有成员的迭代器forEach()
:遍历字典的所有成员
const map = new Map([
['name', 'An'],
['des', 'JS']
]);
console.log(map.entries()) // MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()) // MapIterator {"name", "des"}
console.log(map.values()
复制代码
[图片上传中...(image-35ec22-1554735779698-23)]
<figcaption></figcaption>
const reporter = {
report: function(key, value) {
console.log("Key: %s, Value: %s", key, value);
}
};
let map = new Map([
['name', 'An'],
['des', 'JS']
])
map.forEach(function(value, key, map) {
this.report(key, value);
}, reporter);
// Key: name, Value: An
// Key: des, Value: JS
forEach的第二个参数为reporter对象,那么this就只想这个对象。
复制代码
Map转为其他类型
Map 转 Array
const map = new Map([[1, 1], [2, 2], [3, 3]])
console.log([...map]) // [[1, 1], [2, 2], [3, 3]]
复制代码
Array 转 Map
const map = new Map([[1, 1], [2, 2], [3, 3]])
console.log(map) // Map {1 => 1, 2 => 2, 3 => 3}
复制代码
Map 转 Object
因为 Object 的键名都为字符串,而Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名。
function mapToObj(map) {
let obj = Object.create(null)
for (let [key, value] of map) {
obj[key] = value
}
return obj
}
const map = new Map().set('name', 'An').set('des', 'JS')
mapToObj(map) // {name: "An", des: "JS"}
复制代码
Object 转 Map
function objToMap(obj) {
let map = new Map()
for (let key of Object.keys(obj)) {
map.set(key, obj[key])
}
return map
}
objToMap({'name': 'An', 'des': 'JS'}) // Map {"name" => "An", "des" => "JS"}
复制代码
Map 转 JSON
function mapToJson(map) {
return JSON.stringify([...map])
}
let map = new Map().set('name', 'An').set('des', 'JS')
mapToJson(map) // [["name","An"],["des","JS"]]
复制代码
JSON 转 Map
function jsonToStrMap(jsonStr) {
return objToMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}
复制代码
WeakMap
注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。
属性:
-
constructor:构造函数 方法:
-
has(key):判断是否有 key 关联对象
-
get(key):返回key关联对象(没有则则返回 undefined)
-
set(key):设置一组key关联对象
-
delete(key):移除 key 的关联对象
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
复制代码
总结:
- Set 集合
成员唯一、无序且不重复
[value, value],键值与键名是一致的(或者说只有键值,没有键名)
可以遍历,方法有:add、delete、has
- WeakSet
成员都是对象
成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
不能遍历,方法有add、delete、has
- Map 字典
本质上是键值对的集合,类似集合
可以遍历,方法很多可以跟各种数据格式转换
- WeakMap
只接受对象作为键名(null除外),不接受其他类型的值作为键名
键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
不能遍历,方法有get、set、has、delete
复制代码
四、问题四: 请描述下状态码304?
请描述下状态码304?
复制代码
回答区:
304 表示客户端有缓冲,并且服务端的资源未更新,可以直接使用客户端的,不需要再向服务器获取
复制代码
常用状态码:
[图片上传中...(image-b8ffff-1554735779697-22)]
<figcaption></figcaption>
五、问题五: 写出5种css隐藏元素的办法
写出5种css隐藏元素的办法
复制代码
回答区
opacity: 0;
visibility: hidden;
display: none;
position: absolute; top: -9999px; left: -9999px;
clip-path: polygon(0px 0px,0px 0px,0px 0px,0px 0px); // 剪切掉
复制代码
六、问题六: cookies 与session 有什么区别?
cookies 与session 有什么区别?
复制代码
回答区
Cookies 机制
Http是无状态的协议,因此无法从网络连接上来确定用户的身份,所以就诞生的Cookies。
客户端第一次访问服务器的时候,服务器就会生成一个cookie,并颁发给客户端,这就是你的凭据了。当客户端下次访问的时候就会携带这个Cookies,服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
[图片上传中...(image-b46661-1554735779697-21)]
<figcaption></figcaption>
cookie的内容主要包括name(名字)
、value(值)
、maxAge(失效时间)
、path(路径)
,domain(域)
和secure
.
Cookies 怎么使用在server端的呢?
1、用户初次访问,服务器在response的header中设置`Set-Cookie`,具体值由server端具体决定,这些内容包含一些铭感信息,并不安全。
2、客户端收到响应后,就会在客户端设置cookies
3、下一次访问接口,就会在request带上cookie,
4、server端拿到cookie,查看cookies信息,服务端通过返回的cookie信息去判断本次连接的状态,用户的状态。
5、Cookies可以用在用户登陆后状态的保持
复制代码
Session 机制
Session 是一种服务端机制,存储在服务器中,用一种类似散列表的结构来保存信息。
服务器首先检查这个客户端里的请求里是否已包含了一个session标识--sessionID,如果已经包含一个sessionID,则说明以前已经为此客户端创建过session,服务器就按照sessionID把这个session检索出来使用,如果客户端请求不包含sessionID,则为此客户端创建一个session并且声称一个与此session相关联的sessionID,
因此在客户端只存一个sessionId,每次请求带上这个。
session 怎么使用在server端的呢?
客户端第一次访问,服务端判断传入的cookies中是否含有sessionId,如果有就去sessionData中去查询是否记录过这条id,然后找到对应信息,如果没有就创建一个sessionId,插入到sessionData中。并在response中设置cookie,将sessionId设置进去,存到客户端。客户端下次的时候就会带上了。这里的sessionData保存在server进程内存中,这种方式并不好,我们会用redis去解决。
复制代码
[图片上传中...(image-a2b338-1554735779697-20)]
<figcaption></figcaption>
- cookies
由于http请求是无状态的,需要cookie来做身份验证
1.cookies由服务端创建,发送给浏览器端,存与浏览器端,当用户发出请求后,带上cookie发送给服务端做验证。
2.来回传递过程中,占用带宽,消耗网络资源,并且容易被中间人获取或浏览器端用户篡改十分不安全
3.cookies大小只有4k
4.方便js来操作数据
- session
1.session主要存在于服务端,不会被发送到浏览器所以很安全
2.如果没有设置过期时间,在会话结束后session会自动消失
3.session主要用于服务端保存请求信息的机制
4.服务端会为每一个用户设置一个ID,高效安全。
复制代码
[图片上传中...(image-85d124-1554735779697-19)]
<figcaption></figcaption>
七、问题七: 数组去重
数组去重
复制代码
回答区
我们可以使用不同的方法来实现
// 测试数据 10万数据
const arr = [];
// 生成[0, 100000]之间的随机数
for (let i = 0; i < 100000; i++) {
arr.push(0 + Math.floor((100000 - 0 + 1) * Math.random()))
}
复制代码
【1】双重循环法
// 数组去重
function queue(arr) {
var newArray = [];
var isRepeat = false;
for(var i = 0; i < arr.length; i++) {
// 检查是否重复
isRepeat = false;
for(var j = 0; j < newArray.length; j++) {
if(newArray[j] === arr[i]) {
isRepeat = true;
break;
}
}
if(!isRepeat) {
newArray.push(arr[i]);
}
}
return newArray;
}
var array = [1,2,3,4,5,6,2,3,4];
var array1 = ['1xxx','3','2','3','2', 100];
console.log(queue(array));
console.log(queue(array1));
复制代码
time: 3688.440185546875ms
复制代码
【2】Array.prototype.indexOf()
// 数组去重
Array.prototype.queue = function () {
var newArray = [];
this.forEach((item, index) => {
if(newArray.indexOf(item) == -1) {
newArray.push(item);
}
})
return newArray;
}
var array = [1,2,3,4,5,6,2,3,4];
var array1 = ['1xxx','3','2','3','2', 100];
console.log(array.queue());
console.log(array1.queue());
复制代码
time: test2: 3766.324951171875ms
复制代码
【3】Array.prototype.sort()
// 数组去重
Array.prototype.queue = function () {
var newArray = [];
this.sort();
for (var i = 0; i < this.length; i++) {
if (this[i] !== newArray[newArray.length - 1]) {
newArray.push(this[i]);
}
}
return newArray;
}
var array = [1, 2, 3, 4, 5, 6, 2, 3, 4];
var array1 = ['1xxx', '3', '2', '3', '2', 100];
console.log(array.queue());
console.log(array1.queue());
复制代码
time: 121.6259765625ms
复制代码
【4】Map
// 数组去重
Array.prototype.queue = function () {
const map = new Map();
return this.filter((item, index) => {
return !map.has(item) && map.set(item, 1)
})
}
var array = [1, 2, 3, 4, 5, 6, 2, 3, 4];
var array1 = ['1xxx', '3', '2', '3', '2', 100];
复制代码
或者
// 数组去重
Array.prototype.queue = function () {
var newArray = [];
const map = new Map();
for (var i = 0; i< this.length; i++) {
if( !map.get(this[i])) {
map.set(this[i], 1);
newArray.push(this[i])
}
}
return newArray;
}
var array = [1, 2, 3, 4, 5, 6, 2, 3, 4];
var array1 = ['1xxx', '3', '2', '3', '2', 100];
console.log(array.queue());
console.log(array1.queue());
复制代码
test1: 27.89697265625ms
test2: 21.945068359375ms
复制代码
【5】Set
// 数组去重
Array.prototype.queue = function () {
const set = new Set(this);
return Array.from(set);
}
var array = [1, 2, 3, 4, 5, 6, 2, 3, 4];
var array1 = ['1xxx', '3', '2', '3', '2', 100];
console.log(array.queue());
console.log(array1.queue());
复制代码
// 数组去重
Array.prototype.queue = function () {
return [...new Set(this)];
}
var array = [1, 2, 3, 4, 5, 6, 2, 3, 4];
var array1 = ['1xxx', '3', '2', '3', '2', 100];
console.log(array.queue());
console.log(array1.queue());
复制代码
test1: 36.8046875ms
test2: 31.98681640625ms
复制代码
除了考虑时间复杂度外、性能之外,还要考虑数组元素的数据类型
经过综合考虑,最优的数组去重算法是采用Map数据结构实现的算法。
八、问题八: 将this is a pen首字母大写
将this is a pen首字母大写
复制代码
回答区
var str = "this is a pen";
function ToUpper(str) {
var arr = str.split(" ").map((item, index) => {
return item.slice(0,1).toUpperCase() + item.slice(1)
})
return arr.join(" ");
}
console.log(ToUpper(str)); // This Is A Pen
复制代码
或者
var str = "this is a pen";
function ToUpper(str) {
// 正则匹配
var s = str.toLowerCase().replace(/\b\w+\b/g, function (item) {
return item.slice(0,1).toUpperCase() + item.slice(1)
})
return s;
}
console.log(ToUpper(str)); // This Is A Pen
复制代码
九、问题九: 关于this的题目
关于this的题目
复制代码
案例1
var x = 10;
var obj = {
x: 20,
f: function(){
console.log(this.x); // 20
var foo = function(){
console.log(this.x);
}
foo(); // 10
}
};
obj.f();
复制代码
解析:
var x = 10;
var obj = {
x: 20,
f: function(){
console.log(this.x); // 20 这里是隐性绑定
var foo = function(){
console.log(this.x);
}
foo(); // 10 这里一看是光杆司令,不要误认为foo 在函数 f 里,也在 f 里执行,this必然就跟f里面的this一样。
}
};
obj.f();
复制代码
案例2
function foo(arg){
this.a = arg; // window.a = arg
return this // return window
};
var a = foo(1); // window.a = window
var b = foo(10);
console.log(a.a); // undefined // window.a.a
console.log(b.a); // 10 // window.a
复制代码
案例3
var x = 10;
var obj = {
x: 20,
f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
x: 30,
f: obj.f
}
obj.f(); // 20
bar(); // 10
obj2.f(); // 30
复制代码
十、问题十: 你能描述一下渐进增强和优雅降级吗?
你能描述一下渐进增强和优雅降级吗?
复制代码
在我们开发项目的时候,我们使用的css3的新属性挺多的。最初css3的时候很多浏览器不支持,当时css3标准还是刚出来并没有好的标准,各浏览器厂商按照草案,制定了自己的实现,只是需要在属性前面添加不同浏览器的前缀。当标准确立后,各大浏览器开始逐步支持不带前缀的css3新属性。
下面有两种兼容写法:
.transition {
-webkit-transition: all .5s;
-moz-transition: all .5s;
-o-transition: all .5s;
transition: all .5s;
}
.transition {
transition: all .5s;
-o-transition: all .5s;
-moz-transition: all .5s;
-webkit-transition: all .5s;
}
复制代码
这就引出了两个概念:优雅降级和渐进增强。
渐近增强
渐近增强认为项目应该注重本身,首先应该针对低版本的浏览器实现最基本的功能,之后在针对高级浏览器进行局部调整。换句话说,就是以最低要求,实现最基础功能为基本,向上兼容。 以下写法就是渐近增强。
.transition {
-webkit-transition: all .5s;
-moz-transition: all .5s;
-o-transition: all .5s;
transition: all .5s; // 高浏览器支持
}
复制代码
优雅降级
优雅降级则是首先在高级别浏览器开发,构建整个页面,完善功能。然后针对部分低版本浏览器在做兼容处理。总之便是,高版本为基准,向下兼容。
以下写法就是渐近增强。
.transition {
transition: all .5s;
-o-transition: all .5s;
-moz-transition: all .5s;
-webkit-transition: all .5s;
}
复制代码
渐进增强和优雅降级的区别
-
1、兼容不同 一个是向上兼容,一个是向下兼容。
-
2、成本上分析
如果你采用渐进增强的开发流程,先做一个基本功能版,然后针对各个浏览器进行渐进增加,增加各种功能。相对于优雅降级来说,开发周期长,初期投入资金大。
-
3、技术上分析
- 很久很久以前:浏览器即不宠幸前缀CSS3也不宠幸纯情CSS3(border-radius);
- 不久之前:浏览器只宠幸前缀CSS3,不宠幸纯情的CSS3;
- 现在:浏览器不仅宠幸前缀CSS3属性,还宠幸纯情CSS3属性;
- 等到以后:前缀CSS3就回乡下带孩子了,浏览器只宠幸纯情CSS3属性。
[图片上传中...(image-66c51a-1554735779695-18)]
<figcaption></figcaption>
看上图我们能够区分带前缀属性和不带前缀属性在浏览器的最终处理结果。我们现在正处于 浏览器不仅宠幸前缀CSS3属性,还宠幸纯情CSS3属性;的阶段。
当下,webkit核心的浏览器不仅支持border-radius属性,也支持-webkit-border-radius属性,这本身没什么,只是效果会出现不同。
.content{
width: 200px;
height: 80px;
background: #444;
border-radius: 30px 10px;
-webkit-border-radius: 30px 10px;
margin-bottom: 40px;
}
.content1{
width: 200px;
height: 80px;
background: #444;
-webkit-border-radius: 30px 10px;
border-radius: 30px 10px;
}
<div class="content"></div>
<div class="content1"></div>
复制代码
按理说这两种写法效果应该是一样的,但是我们现在浏览器停留在操蛋的第三阶段,也就是现在,既支持前缀写法,又支持正常写法,这样就要出问题了。
[图片上传中...(image-f71a44-1554735779694-17)]
<figcaption></figcaption>
最终效果上图。我们想要实现的效果是下面的,可是上面是什么鬼?
当属性超过一个参数值的时候,不同属性产生的作用是不一样的!
可以看到,采用优雅降级的写法,如果一个浏览器同时支持前缀写法和正常写法,后面的旧版浏览器样式就覆盖了新版样式,出现一些奇怪的问题 ,但是用渐进增强的写法就不存在这个问题。
建议: 在书写css的时候采用渐近增强的写法。 我们网页项目的开发最好采用优雅降级的方式开发,这样效率更高。当然也不是绝对的,要看你用户使用的浏览器的占比了。
十一、问题十一: CSS 中可以让文字垂直和水平方向上重叠的两个属性是什么?
CSS 中可以让文字垂直和水平方向上重叠的两个属性是什么?
复制代码
回答区:
垂直方向: line-height
水平方向: letter-spacing
复制代码
<style type="text/css">
h1 {letter-spacing: -4px}
h4 {line-height: 0px}
</style>
<body>
<h1>This is header 1</h1>
<h4>This is header 4</h4>
</body>
复制代码
效果如下图:
[图片上传中...(image-884d6c-1554735779694-16)]
<figcaption></figcaption>
<style type="text/css">
p.small {line-height: 100%}
p.small1 {line-height: 90%}
</style>
<body>
<h1>正常高度</h1>
<p>
这是拥有标准行高的段落。
在大多数浏览器中默认行高大约是 110% 到 120%。
这是拥有标准行高的段落。
这是拥有标准行高的段落。
这是拥有标准行高的段落。
这是拥有标准行高的段落。
这是拥有标准行高的段落。
</p>
<h1>100%</h1>
<p class="small">
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
</p>
<h1>90%</h1>
<p class="small1">
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
这个段落拥有更小的行高。
</p>
</body>
复制代码
效果如下图:
[图片上传中...(image-4d333e-1554735779693-15)]
<figcaption></figcaption>
十二、问题十二: 如何解决使用inline-block引起的空白间隙的问题
如何解决使用inline-block引起的空白间隙的问题
复制代码
回答区:
-
方法一: 既然空白间隙是由于换行或空格引起,那么消除换行符不就解决了
-
方法二: 设置父元素的font-size为0,在子元素重新设置字体大小
ie8,firefox,chrome 和 opera 浏览器下已经没有问题了,但是在 低版本safari 浏览器下还是有问题。
-
方法三: 利用负margin-left(不推荐,具体负margin多少取决于字体的大小)
-
方法四: letter-spacing(字符边距):负值 or word-spacing(单词边距) 负值,负值大小还是取决于字体
-
方法五: 【推荐】给父元素 设置font-size:0 ;letter-spacing:-3px ,子元素重新设置font-size。
十三、问题十三: 使用css创建一个三角形
使用css创建一个三角形
复制代码
回答区:
以下是原理图:
[图片上传中...(image-b7251b-1554735779693-14)]
<figcaption></figcaption>
.kailong{
width: 0px;
height: 0px;
border-right: 50px solid transparent;
border-left: 50px solid transparent;
border-bottom: 50px solid red;
}
复制代码
[图片上传中...(image-bb2cb0-1554735779693-13)]
<figcaption></figcaption>
- 实心三角
#demo {
width: 100px;
height: 100px;
background-color: #333;
position: relative;
}
#demo:after {
border: solid transparent;
border-left-color: #333;
border-width: 10px;
width: 0;
content: " ";
position: absolute;
left: 100%;
top: 10%;
}
复制代码
[图片上传中...(image-ee6c16-1554735779693-12)]
<figcaption></figcaption>
- 空心三角
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#demo {
width: 100px;
height: 100px;
border: 1px solid #333;
position: relative;
}
/*小三角*/
#demo:after {
border: solid transparent;
border-left-color: #fff;
border-width: 10px;
content: " ";
position: absolute;
left: 100%;
top: 20px;
/*20px*/
}
/*大三角*/
#demo:before {
border: solid transparent;
border-left-color: #000;
border-width: 12px;
/*10px+2px*/
content: " ";
position: absolute;
left: 100%;
top: 18px;
/*20px-2px*/
}
/* 小三角遮不住大三角 */
</style>
</head>
<body>
<div id="demo"></div>
</body>
</html>
复制代码
[图片上传中...(image-476267-1554735779693-11)]
<figcaption></figcaption>
十四、问题十四: 有一个长度为 100 的数组,请求出该数组的前 10 个元素之和。
有一个长度为 100 的数组,请求出该数组的前 10 个元素之和。
复制代码
回答区:
var arr = [1,2,3,4,5,6,7,8,9,10,23,34,45,56,45,34];
arr.slice(0, 10).reduce((a,b) => {
return a+b
})
// 55
复制代码
十五、问题十五: 写一个程序打印 1 到 100 这些数字,遇到数字为 3 的倍数,打印 “A” 替代该数字;遇到 5 的倍数,用 “B” 代替;遇到即是 3 的倍数又是 5 的倍数,打印 “AB”。
写一个程序打印 1 到 100 这些数字,遇到数字为 3 的倍数,打印 “A” 替代该数字;遇到 5 的倍数,用 “B” 代替;遇到即是 3 的倍数又是 5 的倍数,打印 “AB”。
复制代码
回答区:
for(var i = 1 ; i <= 100; i++) {
if(i%3 == 0 && i % 5 == 0) {
console.log("AB")
} else if (i % 3 == 0){
console.log("A")
} else if (i % 5 == 0) {
console.log("B")
}
}
复制代码
要点: 先排出要求最严格的
十六、问题十六: 引起内存泄漏的情况有?
引起内存泄漏的情况有?
复制代码
- 垃圾回收机
回答区:
十七、问题十七: 写 React / Vue 项目时为什么要在组件中写 key,其作用是什么
写 React / Vue 项目时为什么要在组件中写 key,其作用是什么
复制代码
回答区
要理解key的用法我们就不得不理解一下diff算法了,我们知道,vue和react都实现了一套虚拟DOM,使我们可以不直接操作DOM元素,只操作数据便可以重新渲染页面。而隐藏在背后的原理便是其高效的Diff算法。
diff算法只比较同层的节点,如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。
比如我们有如下情况:
[图片上传中...(image-d533f8-1554735779693-10)]
<figcaption></figcaption>
我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的:
[图片上传中...(image-42bc3a-1554735779693-9)]
<figcaption></figcaption>
在没有key的情况下,会原地复用,修改节点信息,最后还会新增一个节点。
即把C更新成F,D更新成C,E更新成D,最后再插入E,这样效率较低。
diff算法就是利用key值去确定我们的节点,从而能够更快的找到正确的位置区插入新的节点。
[图片上传中...(image-d87dec-1554735779693-8)]
<figcaption></figcaption>
diff算法用于比对新旧虚拟DOM对象,当我们在比较头尾节点无果后,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。如果没找到就认为是一个新增节点。如果找到了就去比对,然后更新节点。
总之: key是给每一个vnode的唯一id,可以依靠key,更准确, 更快的拿到oldVnode中对应的vnode节点。从而是的diff算法能够更快更高效的更新我们的虚拟dom。 参考回答
十八、问题十八: 什么是防抖和节流?有什么区别?
什么是防抖和节流?有什么区别?
复制代码
回答区
防抖和节流都是用来限制我们事件触发的频率,从而提高浏览器的性能。
防抖: 触发事件n秒之后才会执行,但是如果在这个时间到来之前,我们又触发了事件,则清除掉上一次的定时器,然后从新定时。最终实现的效果就是我们连续触发事件时候,只会在最后触发我们的事件函数。
节流: 在防抖的基础上进行改进,允许我们在连续触发的过程中,在固定时间段内可以触发事件函数,所以实际上是在稀释函数的执行频率。
十九、问题十九: 介绍下深度优先遍历和广度优先遍历,如何实现?
介绍下深度优先遍历和广度优先遍历,如何实现?有什么区别?
复制代码
回答区
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="parent">
<div class="child-1">
<div class="child-1-1">
<div class="child-1-1-1">
child-1-1-1
</div>
</div>
</div>
<div class="child-2">
child2
</div>
<div class="child-3">
<div class="child-3-1">
child-3-1
</div>
<div class="child-3-2">
child-3-2
</div>
</div>
</div>
<script>
var a = 10;
// 深度优先搜索
let deepTraversal2 = (node) => {
let nodes = []
if (node !== null) {
nodes.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
nodes = nodes.concat(deepTraversal2(children[i]))
}
}
return nodes
}
// 测试深度
// var dom = document.getElementById('parent');
// console.log(deepTraversal2(dom));
// 广度优先遍历
function widthTraversal2 (node) {
let nodes = [];
let stack = [];
if(node != null) {
stack.push(node);
while(stack.length) {
let item = stack.shift();
let children = item.children
nodes.push(item)
for(var i = 0; i < children.length; i++) {
stack.push(children[i])
}
}
}
return nodes;
}
var dom = document.getElementById('parent');
console.log(widthTraversal2(dom));
</script>
</body>
</html>
复制代码
[图片上传中...(image-a9255e-1554735779693-7)]
<figcaption></figcaption>
二十、问题二十: 你了解EventLoop吗?
你了解EventLoop吗?
复制代码
回答区
众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。
JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
console.log('script end')
复制代码
以上代码虽然 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimeout 还是会在 script end 之后打印。
不同的任务源会被分配到不同的 Task
队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6
规范中,microtask
称为 jobs
,macrotask
称为 task
。
微任务包括
process.nextTick
,promise
宏任务包括
script
,setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
console.log('script start');
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// script start => Promise => script end => promise1 => promise2 => setTimeout
复制代码
首先执行同步代码,遇到promise的话,会首先执行内部的同步代码,然后再继续执行同步代码。途中遇到的settimeout和promise放入不同的任务队列中,这时候由于执行栈已经为空,所以需要开始执行异步任务,首先查看微任务队列,发现又promise已经可以了,那么就执行promise的then,把所有可以执行的微任务都执行完成之后才会去宏任务队列找,发现又setTimeout可以执行了,就执行内部的代码。
所以正确的一次 Event loop 顺序是这样的
执行同步代码,这属于宏任务
执行栈为空,查询是否有微任务需要执行
执行所有微任务
必要的话渲染 UI
然后开始下一轮 Event loop,执行宏任务中的异步代码
复制代码
总结js异步执行机制
JS 主线程拥有一个 执行栈(同步任务) 和 一个 任务队列(microtasks queue),主线程会依次执行代码,
1 首先顺序执行同步代码
2 当遇到task任务(异步)时,会先执行一定的同步任务,然后让主线程继续执行下去,
而真正的task任务将交给浏览器内核 执行,浏览器内核执行结束后,会将该任务事先定义好的回调函数加入相应的任务队列中,
同时设置事件(当回调可以执行的时候通知)
3 当JS主线程清空执行栈之后,会按先入先出的顺序读取microtasks queue中的回调函数,并将该函数入栈,
继续运行执行栈,直到清空执行栈,再去读取任务队列。
4 当microtasks queue中的任务执行完成后,会提取 macrotask queue 的一个任务加入 microtask queue,
接着继续执行microtask queue,依次执行下去直至所有任务执行结束。
这就是 JS的异步执行机制
复制代码
二十一、问题二十一: async await、Promise、setTimeout
async / await、Promise、setTimeout
复制代码
回答区
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
*/
复制代码
在解这个之前我们有几个知识点要清楚:
1、js是单线程的。
2、promise被定义后是立即执行的,但是他的resolve是异步的。
3、promise的异步优先级高于setTimeout。因为promise是微任务
4、async会返回一个promise对象,await关键字会让出线程。
复制代码
接下在我们分析下代码执行流程:
1、执行console.log('script start'),输出script start;
2、执行setTimeout,是一个异步动作,放入异步队列中;
3、执行async1(),输出async1 start,继续向下执行;
4、执行async2(),输出async2,并返回了一个promise对象,await让出了线程,
把返回的promise加入了异步队列,所以async1()下面的代码也要等待上面完成后继续执行;
5、执行 new Promise,输出promise1,然后将resolve放入异步队列;
6、执行console.log('script end'),输出script end;
7、到此同步的代码就都执行完成了,然后去异步队列里去获取任务,现在队列中有一个promise(async2返回的),resolve(new Promise的),和setTimeout,
因为promise属于微任务,先取出promise执行,默认返回resolve,再次加入了异步队列,现在就队列就变成了 `resolve(async2返回的promise返回的)`,`resolve(new Promise的)`,`setTimeout`。
8、接下来执行resolve(async2返回的promise返回的),输出了`async1 end`。
9、然后执行`resolve(new Promise的)`,输出了`promise2`。
10、最后执行`setTimeout`,输出了`settimeout`。
复制代码
题目的本质,就是考察setTimeout、promise、async await的实现及执行顺序,以及JS的事件循环的相关问题。
二十二、问题二十二: Async/Await 如何通过同步的方式实现异步
Async/Await 如何通过同步的方式实现异步
复制代码
回答区
二十三、问题二十三: 算法手写题
已知如下数组:
var arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ],
10
];
编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组
复制代码
回答区
[图片上传中...(image-4f64f-1554735779693-6)]
<figcaption></figcaption>
[图片上传中...(image-9fc7bf-1554735779693-5)]
<figcaption></figcaption>
最后就剩排序了。
二十四、问题二十四: JS异步解决方案的发展历程以及优缺点。
JS异步解决方案的发展历程以及优缺点。
复制代码
回答区
1. 回调函数(callback)
setTimeout(() => {
// callback 函数体
}, 1000)
复制代码
缺点:回调地狱,不能用 try catch 捕获错误,不能 return
回调地狱的根本问题在于:
缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
嵌套函数过多的多话,很难处理错误
复制代码
ajax('XXX1', () => {
// callback 函数体
ajax('XXX2', () => {
// callback 函数体
ajax('XXX3', () => {
// callback 函数体
})
})
})
复制代码
- 优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
2. Promise
Promise就是为了解决callback的问题而产生的。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
优点:解决了回调地狱的问题
ajax('XXX1')
.then(res => {
// 操作逻辑
return ajax('XXX2')
}).then(res => {
// 操作逻辑
return ajax('XXX3')
}).then(res => {
// 操作逻辑
})
复制代码
缺点:无法取消 Promise ,错误需要通过回调函数来捕获
3. Async/await
async、await 是异步的终极解决方案
优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch('XXX1')
await fetch('XXX2')
await fetch('XXX3')
}
复制代码
async 函数返回的 Promise 对象,必须等到内部所有的 await 命令的 Promise 对象执行完,才会发生状态改变
二十五、问题二十五: 面试可能会问到的一些网络请求问题
面试可能会问到的一些网络请求问题
复制代码
1、关于网络请求的疑问
- Ajax的出现解决了什么问题
- 原生Ajax如何使用
- jQuery的网络请求方式
2、Ajax的出现解决了什么问题
在Ajax出现之前,web程序是这样工作的:
[图片上传中...(image-d2038-1554735779692-4)]
<figcaption></figcaption>
这种交互的的缺陷是显而易见的,任何和服务器的交互都需要刷新页面,用户体验非常差,Ajax的出现解决了这个问题。
使用Ajax,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。
ajax可以实现,我们发送请求,获取相应的数据,然后通过js去动态渲染页面,而不需要服务器拼接HTML,页面的刷新也只是局部的刷新,不再是整个页面的刷新了。
3、原生Ajax的用法
手写一个原生的ajax
var xhr = new XMLHttpRequest();
xhr.open('post', 'http://', true);
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
console.log(xhr.responseText);
}
}
}
let postData = {"name1":"value1","name2":"value2"};
postData = (function(value) {
var dataString = "";
for(var key in value){
dataString += key+"="+value[key]+"&";
};
return dataString;
})(postData);
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
// 异常处理
xhr.onerror = function() {
console.log('Network request failed')
}
// 跨域携带cookie
xhr.withCredentials = true;
xhr.send(postData);
复制代码
[图片上传中...(image-1857ce-1554735779692-3)]
<figcaption></figcaption>
- readyState
用来标识当前XMLHttpRequest对象所处的状态,XMLHttpRequest对象总是位于下列状态中的一个:
[图片上传中...(image-6b8f15-1554735779692-2)]
<figcaption></figcaption>
- status
表示http请求的状态, 初始值为0。如果服务器没有显式地指定状态码, 那么status将被设置为默认值, 即200。
- responseType
表示响应的数据类型,并允许我们手动设置,如果为空,默认为text类型,可以有下面的取值:
[图片上传中...(image-170758-1554735779692-1)]
<figcaption></figcaption>
- onreadystatechange
当readyState属性发生变化时,callback会被触发。
- onprogress
xhr.onprogress = function(event){
console.log(event.loaded / event.total);
}
复制代码
回调函数可以获取资源总大小total,已经加载的资源大小loaded,用这两个值可以计算加载进度。
- onerror
当ajax资源加载失败时会触发callback。
4、jQuery对Ajax的封装
在很长一段时间里,人们使用jQuery提供的ajax封装进行网络请求,包括[图片上传中...(image-f6a52e-1554735779692-0)]
.get、$.post等,这几个方法放到现在,我依然觉得很实用。
$.ajax({
dataType: 'json', // 设置返回值类型
contentType: 'application/json', // 设置参数类型
headers: {'Content-Type','application/json'},// 设置请求头
xhrFields: { withCredentials: true }, // 跨域携带cookie
data: JSON.stringify({a: [{b:1, a:1}]}), // 传递参数
error:function(xhr,status){ // 错误处理
console.log(xhr,status);
},
success: function (data,status) { // 获取结果
console.log(data,status);
}
})
复制代码
二十六、问题二十六: 介绍模块化发展历程
跨域方案
复制代码
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。
【1】IIFE 使用自执行函数来编写模块化
(function(){
return {
data:[]
}
})()
复制代码
特点: 在一个单独的函数作用域中执行代码,避免变量冲突。
【2】AMD 使用requireJS 来编写模块化
define('./index.js',function(code){
// code 就是index.js 返回的内容
})
复制代码
特点: 依赖必须提前声明好。
【3】CMD 使用seaJS 来编写模块化
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
复制代码
特点: 支持动态引入依赖文件。
【4】CommonJS nodejs 中自带的模块化
var fs = require('fs');
复制代码
【5】ES Modules ES6 引入的模块化,支持import 来引入另一个 js 。
import a from 'a';
作者:meils
链接:https://juejin.im/post/5c8207fae51d453c8f4860e1
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。