web基础

作为一个web开发者,基于HTML、CSS和JS的web基础知识点,不管是初级码农,还是骨灰级专家,在面试时都可能问到。

一个完整的js实现包括啥?

ECMAscript, DOM, BOM

DOCTYPE有什么作用?

用于声明文档使用哪种规范,告诉浏览器使用哪个版本的HTML规范来渲染文档。

行内、块级、空元素各有哪些?

  • 行内元素:a span img input select
  • 块级元素:div ul ol li dl dt dd h1 p
  • 空元素:br hr link meta

标签中src和href的区别?

  • src 指向外部资源的位置,请求src资源时会将其下载并应用到文档内;
  • href 指向网络资源所在位置,建立和当前元素(锚点)或当前文档(链接)之间的链接,用于超链接。

HTML5有哪些优势?

  • 更多的语义化标签:header、footer、nav、hgroup、article、section、time等;
  • 对媒体支持:使用audio和video标签避免先前以插件的方式播放音频、视频带来的麻烦;
  • Canvas绘图:实现在HTML页面中绘制图形和图像,且所有的绘图内容都是使用js来控制的;
  • SVG绘图技术:矢量图技术,可以无限缩放;
  • 实时通信:HTML5提供了对Web Sockets的支持;
  • Geolocation: 地理定位,使用浏览器获得客户端所在的地理坐标;
  • 客户端本地存储:会话级客户端存储sessionStorage和跨会话级客户端存储localStorage;
  • 拖放API:在HTML页面中实现GUI程序中的“拖”和“放”操作,提供了七个新事件;
  • 文件离线储存:浏览器就会根据manifest文件的内容下载相应的资源,并进行离线存储。

为什么要重置 浏览器 的css默认属性?

兼容问题,不同浏览器对一些标签的CSS默认值是不同的,如果没对CSS初始化会出现浏览器之间的页面显示差异。

CSS如何计算选择器优先?

  • 相同权重,定义最近者为准:行内样式 > 内部样式 > 外部样式;
  • 含外部载入样式时,后载入样式覆盖其前面的载入的样式和内部样式;
  • 选择器优先级: 行内样式[1000] > id[100] > class[10] > Tag[1];
  • 在同一组属性设置中,!important 优先级最高,高于行内样式;
  • 继承得到的样式的优先级最低。

介绍使用过的 CSS 预处理器?

  • CSS 预处理器的原理:为 CSS 增加了一些编程的特性(变量、逻辑判断、函数等),开发时使用这种方式进行页面的样式设计,部署时再编译成普通的 CSS 文件;
  • 使用 CSS 预处理器的好处:可以使 CSS 更加简洁、适应性更强、可读性更佳,无需考虑兼容性;
  • 最常用的 CSS 预处理器语言包括:Sass(SCSS)和 LESS, stylus。

var const let 的区别?

  • 初始值:const 声明的变量必须设置初始值,且不能重复赋值。
  • 重复定义:const 和 let 不支持重复定义
  • const,let 支持块级作用域,有效避免变量覆盖
  • 变量提升:const 和 let 必须先声明再使用,不支持变量提升

原始类型有哪几种?

在 JS 中,原始类型有:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol

其特点有:

  • 原始类型存储的都是值,是没有函数可以调用的,比如 null.toString(), '100'.toString() 可以使用是因为它被强制转换成了 String 内置对象的类型,所以能调用 toString 函数。
  • JS 的 number 类型是浮点类型的,存在 0.1 + 0.2 !== 0.3 的问题。

null 是对象嘛?

  • 在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object,这成了 JS 的一个悠久 Bug。

谈谈 typeof ?

  • typeof 对于原始类型来说,除了 null 都可以显示正确的类型:
typeof undefined // 'undefined'
typeof null // 'object'
typeof 0 // 'number'
typeof '0' // 'string'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
  • typeof 对于引用类型来说,除了函数都会显示 object:
typeof [] // 'object'
typeof {} // 'object'
typeof JSON.parse; // 'function'

可以看到,单独使用 typeof 并不能准确判断变量的类型,要想判断一个变量的类型,需要结合多种手段。

如何判断一个变量是否为数组?

  • 内置数组对象上 Array.isArray() 方法;
  • instanceof 此方法返回一个布尔值;
  • Array.prototype.isPrototypeOf(): 用于指示对象是否存在于一个对象的原型链中,如果存在返回true,反之返回false;
  • 构造函数 constructor: variable.constructor.toString().indexOf("Array");
  • Object.prototype.toString.call(variable).indexOf('Array'): 调用toString时会将this对象的[[class]]属性值拿到,而这个属性值就是该对象的真实类型。

this几种不同的使用场景?

  • 在构造函数中使用(构造函数本身)
  • 作为对象属性时使用(调用属性的对象)
  • 作为普通函数时使用(window)
  • call,apply,bind(执行的第一个参数)

函数声明与函数表达式的区别?

  • 在Javscript中,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非是一视同仁的,解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);
  • 至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解析执行。

谈谈闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的常见方式为在一个函数内部创建另一个函数。

闭包存在的意义主要是让我们可以间接访问函数内部的变量,弥补JS设计上不够安全的缺陷。

最典型的例子就是如下:

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), i * 1000);
}

我们期待是打印出0,1,2,3,4,最后却打印出了 5个5,这是因为 setTimeout 是个异步函数,会先把循环全部执行完毕再打印,此时 i 就是 5了。

解决办法有很多,比如使用ES6的let替代var,实现块级作用域,兼容性最好的还是使用闭包:

for (var i = 0; i < 5; i++) {
  (function(j) {
      setTimeout(() => console.log(j), j* 1000);
    )(i)
}

使用立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 打印 的闭包函数时,就可以使用外部函数的变量 j,从而达到目的。

谈谈浅拷贝?

浅拷贝主要是为了解决使用赋值方式复制引用类型的数据时,改变新变量的值,也会影响原有变量的情况,比如:

let x = {
  a: 0
};
let y = x;
y.a = 6
console.log(x.a) // 6

这是因为使用赋值复制时,JS引擎没有分配新内存,新变量指向的还是原变量所存指针的地址,解决这个问题可以使用浅拷贝:

  • 通过 Object.assign 来实现:
let x = {
  a: 0
};
let y = Object.assign({}, x);
y.a = 6
console.log(x.a) // 0
  • 通过展开运算符 ... 来实现:
let x = {
  a: 0
};
let y = { ...x };
y.a = 6
console.log(x.a) // 0

谈谈深拷贝

为何会有深拷贝,这是因为浅拷贝不能解决对象属性中包含对象属性的问题,

let x = {
  a: 0,
  b: {
    c: 5
  }
};
let y = { ...x };
y.b.c = 6
console.log(x.b.c) // 6

解决这个问题的方式我们称为深拷贝:

  • 使用JSON.parse(JSON.stringify()) :有忽略 undefined和symbol、不能序列化函数及不能解决循环引用的局限性;
  • 使用 MessageChannel:要求所拷贝的对象含有内置类型且不包含函数;

实现项目中的深拷贝函数,需要考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等,一般都使用 lodash 的深拷贝函数,这里我们只是实现简易版应付面试:

const deepClone = obj => {
 let newObj = Array.isArray(obj) ? [...obj] : { ...obj }
  Reflect.ownKeys(newObj).forEach(key => {
    newObj[key] = (typeof obj === 'object' || typeof obj === 'function') && obj !== null ? deepClone(obj[key]) : obj[key];
  })
  return newObj;
}

const x = {
  a: 0,
  b: {
    c: 5
  }
};
const y = deepClone(x);
y.b.c = 6
console.log(x.b.c) // 0

谈谈组合继承?

组合继承,指的是将原型链和借用构造函数的技术组合到一起, 避免了原型链和借用构造函数的缺点,是JS中最常用的继承模式:

  • 思路是使用原型链实现对原型方法的继承,而通过借用构造函数来实现对实例属性的继承。
  • 既通过在原型上定义方法实现了函数的复用,又能够保证每个实例都有它自己的属性。
function SuperType(name) {
  this.name=name;
  this.colors=["red", "blue", "green"];
}
SuperType.prototype.sayName=function() {
  alert(this.name);
};

function SubType(name, age) {
  //借用构造函数实现对实例属性的继承
  SuperType.call(this,name);
  this.age=age;
}

//继承方法   使用原型链实现
SubType.prototype=new SuperType();
SubType.prototype.constructor=SubType;
subType.prototype.sayAge=function() {
  alert(this.age);
};

const instance1=new SubType("mary", 22);
instance1.colors.push("black");

alert(instance1.colors);   //red,blue,green,black
instance1.sayName();  //mary
instance1.sayAge();  //22

const instance2=new SubType("greg", 25);
alert(instance2.colors);   //red,blue,green
instance2.sayName();  //greg
instance2.sayAge();  //25

小结

对于工作3年左右的JS工程师,以上的问题,我在几年前的面试过程中基本都被问到,近两年因为看机会基本都是架构师或者负责人,很少会问到。