JS 面试

195 阅读4分钟

1.js数据类型

基本类型(单类型):String、Number、boolean、null、undefined,symbol。 引用类型:object。里面包含的 function、Array、Date。 ES6 中新增了一种 Symbol 。这种类型的对象永不相等,即始创建的时候传入相同的值,可以解决属性名冲突的问题,做为标记。 谷歌67版本中还出现了一种 bigInt。是指安全存储、操作大整数。(但是很多人不把这个做为一个类型)。

2.判断 js 类型的方式

1.typeof

可以判断出'string','number','boolean','undefined','symbol' 但判断 typeof(null) 时值为 'object'; 判断数组和对象时值均为 'object'

2.instanceof

原理是 构造函数的 prototype 属性是否出现在对象的原型链中的任何位置
用法:
function A() {}
let a = new A();
a instanceof A     //true,因为 Object.getPrototypeOf(a) === A.prototype;
  1. Object.prototype.toString.call()
常用于判断浏览器内置对象,对于所有基本的数据类型都能进行判断,即使是 nullundefined
用法:
Object.prototype.toString.call('') ==> "[object String]"
Object.prototype.toString.call([]) ==> "[object Array]"
  1. Array.isArray()
用于判断是否为数组
用法:
Array.isArray([])

3.浅拷贝和深拷贝

1.浅拷贝

// 当数据只有一层为深拷贝
Object.assign()
Array.prototype.slice()
扩展运算符 ...

2.深拷贝

JSON.parse(JSON.stringify())   不能拷贝function,不符合json对象
lodash.deepClone()
手写深拷贝方法,递归遍历方法

4.数组去重的方法

1.ES6 Set

let arr = [1,1,2,3,4,5,5,6]
let arr2 = [...new Set(arr)]

2.reduce()

let arr = [1,1,2,3,4,5,5,6]
let arr2 = arr.reduce(function(ar,cur) {
  if(!ar.includes(cur)) {
    ar.push(cur)
  }
  return ar
},[])

3.filter()

// 这种方法会有一个问题:[1,'1']会被当做相同元素,最终输入[1]
let arr = [1,1,2,3,4,5,5,6]
let arr2 = arr.filter(function(item,index) {
  // indexOf() 方法可返回某个指定的 字符串值 在字符串中首次出现的位置
  return arr.indexOf(item) === index
})

5.['1','2','3'].map(parseInt)

parseInt方法接收两个参数,参数为map(item, index, arr)里的前面两个(item, index)
parseInt('1', 0) ECMAScript5将string作为十进制数字的字符串解析;=>1
parseInt('2', 1) =>NaN
parseInt('3', 2) =>NaN

6.什么是闭包(closure),为什么要用它?

闭包指的是一个函数可以访问另一个函数作用域中变量。常见的构造方法,是在一个函数内部定义另外一个函数。内部函数可以引用外层的变量;外层变量不会被垃圾回收机制回收。 注意,闭包的原理是作用域链,所以闭包访问的上级作用域中的变量是个对象,其值为其运算结束后的最后一个值。 优点:避免全局变量污染。缺点:容易造成内存泄漏。

7.AMD(Modules/Asynchronous-Definition)、CMD(Common Module Definition)规范区别?

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。 区别:

  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。
  2. CMD 推崇依赖就近,AMD 推崇依赖前置。
  3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。
// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    // 此处略去 100 行
    var b = require('./b') // 依赖可以就近书写
    b.doSomething()
})
// AMD 默认推荐
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething();
    // 此处略去 100 行
    b.doSomething();
})

8.call和apply

call()方法和appl()方法的作用相同,动态改变某个类的某个方法的运行环境。他们的区别在于接收参数的方式不同。在使用call()方法时,传递给函数的参数必须逐个列举出来。使用apply()时,传递给函数的是参数数组。

9.回流与重绘

当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树。完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘

10.ajax请求的时候get 和post方式的区别

get: 参数拼接再url后面,post放在虚拟载体里面,get有大小限制(只能提交少量参数),安全问题,应用不同 ,请求数据和提交数据。

11.清除字符串前后的空格

String.prototype.trim= function(){
  return this.replace(/^\s+/,"").replace(/\s+$/,"");
}

12.dom事件委托什么原理,有什么优缺点

事件委托原理:事件冒泡机制
优点:
1.可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件就很不错。
2.可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适
缺点:
事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。

13.理解web安全吗?都有哪几种,介绍以及如何预防

1.XSS,也就是跨站脚本注入

攻击方法:
1. 手动攻击:
编写注入脚本,比如”/><script>alert(document.cookie());</script><!--等,
手动测试目标网站上有的input, textarea等所有可能输入文本信息的区域
2. 自动攻击
利用工具扫描目标网站所有的网页并自动测试写好的注入脚本,比如:Burpsuite等
防御方法:
1. 将cookie等敏感信息设置为httponly,禁止Javascript通过document.cookie获得
2. 对所有的输入做严格的校验尤其是在服务器端,过滤掉任何不合法的输入,比如手机号必须是数字,通常可以采用正则表达式
3. 净化和过滤掉不必要的html标签,比如:<iframe>, alt,<script>4. 净化和过滤掉不必要的Javascript的事件标签,比如:onclick, onfocus等
5. 转义单引号,双引号,尖括号等特殊字符,可以采用htmlencode编码 或者过滤掉这些特殊字符
6. 设置浏览器的安全设置来防范典型的XSS注入

2.SQL注入

攻击方法:
编写恶意字符串,比如‘ or  1=1--等,
手动测试目标网站上所有涉及数据库操作的地方
防御方法:
1. 禁止目标网站利用动态拼接字符串的方式访问数据库
2. 减少不必要的数据库抛出的错误信息
3. 对数据库的操作赋予严格的权限控制
4. 净化和过滤掉不必要的SQL保留字,比如:where, or, exec 等
5. 转义单引号,上引号,尖括号等特殊字符,可以采用htmlencode编码 或者过滤掉这些特殊字符

3.CSRF,也就是跨站请求伪造

就是攻击者冒用用户的名义,向目标站点发送请求
防范方法:
1. 在客户端进行cookie的hashing,并在服务端进行hash认证
2. 提交请求是需要填写验证码
3. 使用One-Time Tokens为不同的表单创建不同的伪随机值  

14.HTTP 304状态码的详细讲解

304(未修改):自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。
如果网页自请求者上次请求后再也没有更改过,您应将服务器配置为返回此响应(称为 If-Modified-Since HTTP 标头)。服务器可以告诉 Googlebot 自从上次抓取后网页没有变更,进而节省带宽和开销。

整个请求响应过程如下:

客户端在请求一个文件的时候,发现自己缓存的文件有 Last Modified ,那么在请求中会包含 If Modified Since ,这个时间就是缓存文件的 Last Modified 。因此,如果请求中包含 If Modified Since,就说明已经有缓存在客户端。服务端只要判断这个时间和当前请求的文件的修改时间就可以确定是返回 304 还是 200 。
对于静态文件,例如:CSS、图片,服务器会自动完成 Last Modified 和 If Modified Since 的比较,完成缓存或者更新。但是对于动态页面,就是动态产生的页面,往往没有包含 Last Modified 信息,这样浏览器、网关等都不会做缓存,也就是在每次请求的时候都完成一个 200 的请求。
因此,对于动态页面做缓存加速,首先要在 Response 的 HTTP Header 中增加 Last Modified 定义,其次根据 Request 中的 If Modified Since 和被请求内容的更新时间来返回 200 或者 304 。虽然在返回 304 的时候已经做了一次数据库查询,但是可以避免接下来更多的数据库查询,并且没有返回页面内容而只是一个 HTTP Header,从而大大的降低带宽的消耗,对于用户的感觉也是提高。当这些缓存有效的时候,通过 Fiddler 或HttpWatch 查看一个请求会得到这样的结果:

第一次访问 200
按F5刷新(第二次访问) 304
按Ctrl+F5强制刷新 200

15

var a ={n:1};
var b = a;
a.x=a={n:2};
console.log(a); // {n:2}  a.x = undefined
console.log(b); // {n: 1,x:{n: 2}}  b.x = {n: 2}
1、首先对a开辟一块内存空间,然后存入{n:1},然后对b赋值a的地址,此时ab指向同一块地址
2、.运算符优先于 = 号运算符,此时先执行.操作,即在a的地址内放入x属性(此处不同人有不同理解,我暂时理解为属性名不是变量,没有开辟新的空间)
3、此时才进行赋值运算,赋值从右往左,一次赋值,即把{n:2}赋值给aa.x,此时a指向新地址{n:2},而a.x也指向新地址{n:2}
4、也就是a.x被追加到了原地址a,即ba指向新地址

16. 解析URL参数

getQuery () {
  const url = decodeURI(location.search) // 获取url中"?"符后的字串(包括问号)
  let query = {}
  if (url.includes('?')) {
    const str = url.substr(1) // 删除问号
    const pairs = str.split('&')
    pairs.forEach(item => {
      const pair = item.split('=')
      query[pair[0]] = pair[1]
    })
  }
  return query ;  // 返回对象
}
数组形式转成url
let arr = [
  {name: 'name', value: 2},
  {name: 'id', value: 1}
]
arrayToUrl (arr) {
  if (arr.length) {
    let str = ''
    arr.forEach(item => {
      str += ((str ? '&' : '?') + `${item.name}=${item.value}`)
    })
    return str
  }
}
arrayToUrl(arr)