javaScript

1.js 的基本类型有哪些?引用类型有哪些?null 和 undefined 的区别

基础数据类型

undefined、null、boolean、number、string

引用数据类型

function、object、array

null 和 undefined 的区别

TIP

javaScript(ECMAScript 标准)里共有 5 种基本类型:Undefined, Null, Boolean, Number, String,和一种复杂类型 Object。可以看到 null 和 undefined 分属不同的类型,未初始化定义的值用 typeof 检测出来是"undefined"(字符串),而 null 值用 typeof 检测出来是"object"(字符串)。任何时候都不建议显式的设置一个变量为 undefined,但是如果保存对象的变量还没有真正保存对象,应该设置成 null。实际上,undefined 值是派生自 null 值的,ECMAScript 标准规定对二者进行相等性测试要返回 true

  1. undefined:表示变量声明但未初始化时的值
  2. null 表示准备用来保存对象,还没有真正保存对象的值。从逻辑角度看,null 值表示一个空对象指针

2.如何判断一个变量是 Array 类型?如何判断一个变量是 Number 类型

  1. 从原型入手,Array.prototype.isPrototypeOf(obj)也可以从构造函数入手,obj instanceof Array根据对象的class属性(类属性),跨原型链调用toString()方法。Array.isArray()方法。
  2. isNaN()是一个函数,用 isNaN 判断一个变量,返回一个Boolean值。若返回的值为 false,则为可以转换成数字类型;返回的值是 true,则不能转换成数字类型
  3. typeof() 判断

3.Object 是引用类型嘛?引用类型和基本数据类型有什么区别?堆栈关系了解吗

Object 是引用类型。

基本类型

  1. 基本类型的值是不可变得
  2. 基本类型的比较是值的比较
  3. 基本类型的变量是存放在栈区的(栈区指内存里的栈内存)

引用类型

  1. 引用类型的值是可变的
  2. 引用类型的值是同时保存在栈内存和堆内存中的对象

引用类型与基本类型比较

基本类型:string,number,boolean,null,undefined引用类型:Function,Array,Object
访问方式
操作和保存在变量的实际的值存在内存中,js 不许直接访问内存,操作的是对象的引用
存储的位置
保存在栈区 引用存放在栈区,实际对象保存在堆区

4.解释一下事件冒泡和事件捕获

  1. 事件冒泡:当你使用事件捕获时,父级元素先触发,子级元素后触发
  2. 事件捕获:当你使用事件冒泡时,子级元素先触发,父级元素后触发

5.事件委托,事件冒泡和捕获,如何阻止冒泡,如何阻止默认事件

事件委托:

var toolbar = document.querySelector('.toolbar')
toolbar.addEventListener('click', function (e) {
  let button = e.target
  if (!button.classList.contains('active')) {
    button.classList.add('active')
  } else {
    button.classList.remove('active')
  }
})

事件冒泡,就是元素自身的事件被触发后,如果父元素有相同的事件,如onclick事件,那么元素本身的触发状态就会传递,也就是冒到父元素,父元素的相同事件也会一级一级根据嵌套关系向外触发,直到document/window,冒泡过程结束

但是事件冒泡在某些应用场景产生一些问题,就是我们不需要触发的事件,由于冒泡的原因,也会运行。所以在这个时候要取消事件冒泡。阻止事件冒泡如下:

box.onmouseover = function (event) {
  // 阻止冒泡
  event = event || window.event
  if (event && event.stopPropagation) {
    event.stopPropagation()
  } else {
    event.cancelBubble = true
  }
}

事件捕获,与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。事件捕获的概念下发生click事件的顺序应该是document -> html -> body -> div -> p阻止事件冒泡如下:

// 阻止浏览器的默认行为
function stopDefault(e) {
  // 阻止默认浏览器动作(W3C)
  if (e && e.preventDefault) {
    e.preventDefault()
  } else {
    window.event.returnValue = false
  }
  return false
}

6.对闭包的理解?什么时候构成闭包?闭包的实现方法?闭包的优缺点

TIP

函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。闭包就是能够读取其他函数内部变量的函数。内部函数对外部函数的变量有了引用关系——闭包就是这时产生的。每次对外部函数的调用,都会产生一次闭包

实现方法

  1. 给函数添加一些属性
  2. 声明一个变量,将一个函数当做值赋给变量
  3. new一个对象,然后给对象添加属性和方法
  4. var obj={}就是声明一个空的对象

用处

它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在 f1 调用后被自动清除。

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  2. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

7.this 有哪些使用场景?跟 JAVA 中的 this 有什么区别?如何改变 this 的指向

this 的使用场景

  1. 全局&调用普通函数,在全局环境中,this 永远指向 window
  2. 构造函数,如果函数作为构造函数使用,那么其中的 this 就代表它即将 new 出来的对象
  3. 对象方法,如果函数作为对象的方法时,方法中的 this 指向该对象。注意:若是在对象方法中定义函数,那么情况就不同了。函数 a 虽然是在 b 内部定义的,但它仍然属于一个普通函数,this 仍指向 window
  4. 构造函数 prototype 属性,即便是在整个原型链中,this 代表的也是当前对象的值
  5. 函数用 callapply或者 bind 调用,当一个函数被 callapply 或者 bind 调用时,this 的值就取传入的对象的值
  6. DOM event this,前六种情况其实可以总结为: this 指向调用该方法的对象
  7. 箭头函数中的 this,当使用箭头函数的时候,情况就有所不同了:箭头函数内部的 this 是词法作用域,由上下文确定

跟 JAVA 中的 this 有什么区别

javathis.value可以再本类中调用全局变量,也可以在构造器中用this()调用其他构造器,也可以用this表示当前对象 JavaScript 中this指的是这个函数所属的对象的值,当new一个函数时,这个 this 就会指向这个 new 出来的对象,apply()call()可以改变一个函数中this指向的对象callapply都可以改变this指向,不过call的第二个参数是散列分布,apply则可以是一个数组

8.call,apply,bind 有什么区别

  1. call()apply()的第一个参数相同,就是指定的对象。这个对象就是该函数的执行上下文。
  2. call()在第一个参数之后的 后续所有参数就是传入该函数的值。
  3. apply() 只有两个参数,第一个是对象,第二个是数组,这个数组就是该函数的参数。
  4. bind() 方法和前两者不同在于: bind() 方法会返回执行上下文被改变的函数而不会立即执行,而前两者是直接执行该函数。他的参数和 call()相同。

9.变量提升

javaScript 中,函数及变量的声明都将被提升到函数的最顶部 javaScript 中,变量可以在使用后声明,变量允许先使用再进行声明 javaScript 只有声明的变量会提升,初始化的不会

10.typeof 能得到哪些值

number、boolean、string、undefined、object、function

11.匿名函数典型用例

// 1.无参匿名函数
;(function () {
  console.log('-----')
})()

// 2.携参匿名函数
;(function (params) {
  console.log(params)
})({ id: '', name: '' })

12.创建对象有几种方式

  1. 通过{}创建对象
// 使用 {} 创建对象,等同于 new Object()
let obj = {}
obj.name = '测试'
obj.age = 20

obj.sayName = function () {
  console.log(this.name)
}
console.log(obj.name + '-' + obj.age)
obj.sayName()
  1. new Object()
let obj = new Object() // 创建对象
obj.name = '测试'
obj.age = 20

obj.sayName = function () {
  console.log(this.name)
}
obj.sayName()
console.log(obj instanceof Object) // true
console.log(typeof obj) // object
  1. 使用字面量
var person = { name: 'zhang', age: 20 }

TIP

前面三种创建对象的方式存在 2 个问题: 1.代码冗余; 2.对象中的方法不能共享,每个对象中的方法都是独立的

  1. 工厂模式

工厂模式创建对象,减少重复代码,解决代码冗余问题,但不能共享对象

优点:【解决了代码重复问题】缺点:【调用的还是不同的方法】

// 定义工厂方法
function createObjectFactory(name) {
  let obj = new Object()
  obj.name = name
  obj.sayName = function () {
    console.log(this.name)
  }
  return obj
}
let a = createObjectFactory('zhang')
let b = createObjectFactory('liu')
console.log(a.sayName === b.sayName) // false
  1. 构造函数模式(constructor

构造函数:用 new 关键字来进行调用的函数称为构造函数,一般首字母要大写

// 创建构造函数
function Person(name) {
  this.name = name
  this.sayName = function () {
    console.log(this.name)
  }
}

var p1 = new Person('zhang')
var p2 = new Person('li')

p1.sayName()
p2.sayName()

console.log(p1.constructor === p2.constructor) //true
console.log(p1.constructor === Person) //true

console.log(typeof p1) //object

console.log(p1 instanceof Object) //true
console.log(p2 instanceof Object) //trueb

console.log(p1.sayName === p2.sayName) //false
  1. 原型模式(prototype

每个方法中都有一个原型(prototype),每个原型都有一个构造器(constructor),构造器又指向这个方法

function Animal() {}

Animal.prototype.name = 'animal'
Animal.prototype.sayName = function () {
  alert(this.name)
}

var a1 = new Animal()
var a2 = new Animal()

a1.sayName()

console.log(a1.sayName === a2.sayName) //true
console.log(Animal.prototype.constructor) //function Animal(){}
console.log(Animal.prototype.constructor == Animal) //true
  1. 组合使用:构造模式+原型模式

结合了上面两种方式,解决了代码冗余,方法不能共享,引用类型改变值的问题

function Animal(name) {
  this.name = name
  this.friends = ['dog', 'cat']
}
Animal.prototype.sayName = function () {
  console.log(this.name)
}
var a1 = new Animal('d')
var a2 = new Animal('c')
a1.friends.push('snake')
console.log(a1.friends) //[dog,cat,snake]
console.log(a2.friends) //[dog,cat]

13.document loaddocument DOMContentLoaded两个事件之前的区别

TIP

区别:触发时机不一样,先触发 DOMContentLoaded事件,后触发 load事件

DOM 文档加载的步骤:

  1. 解析 HTML 结构
  2. 加载外部脚本和样式表文件
  3. 解析并执行脚本代码
  4. DOM 树构建完成,DOMContentLoaded 事件触发
  5. 加载图片等外部文件
  6. 页面加载完毕,load 事件触发

14.New 一个对象具体做了什么

使用关键字 new 创建新实例对象经过了以下几步

  1. 创建一个新对象,如:var person = {}
  2. 新对象的 _proto_ 属性执行构造函数的原型对象
  3. 将构造函数的作用域赋值给新对象(所以 this 对象指向新对象)
  4. 执行构造函数内部的代码,将书香添加给 person 中的 this 对象
  5. 返回新对象person

15.js 的参数使用什么方式进行传递的

基础类型的传递方式比较简单,是按照值传递进行的

let a = 1
function test(x) {
  x = 10 // 并不会改变实参值
  console.log(x)
}
test(a) // 10
console.log(a) // 1

复杂类型,传递的是地址

let a = {
  count: 1
}
function test(x) {
  x.count = 10
  console.log(x)
}
test(a) // {count: 10}
console.log(a) // {count: 10}

16.javaScript 垃圾回收

js 中的内存分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。如果不关注 js 内存管理问题,不了解 js 内存管理机制,同样容易造成内存泄露(内存无法被回收)的情况

内存的生命周期

js 环境中分配的内存,一般有如下生命周期:

  1. 内存分配:声明变量、函数、对象的时候,系统会自动为其分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由来及回收自动回收不再使用的内存

内存分配

// 为变量分配内存
let a = 11
let b = 'code'

// 为对象分配内存
let person = {
  name: 'code',
  age: 20
}

// 为函数分配内存
function sum(a, b) {
  return a + b
}

垃圾回收算法说明

所谓垃圾回收, 核心思想就是如何判断内存是否已经不再会被使用了, 如果是, 就视为垃圾, 释放掉

下面介绍两种常见的浏览器垃圾回收算法: 引用计数 和 标记清除法

引用计数

IE 采用的引用计数算法, 定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。

如果没有任何变量指向它了,说明该对象已经不再需要了。

// 创建一个对象person, person指向一块内存空间, 该内存空间的引用数 +1
let person = {
  age: 22,
  name: 'ifcode'
}

let p = person // 两个变量指向一块内存空间, 该内存空间的引用数为 2
person = 1 // 原来的person对象被赋值为1,对象内存空间的引用数-1,
// 但因为p指向原person对象,还剩一个对于对象空间的引用, 所以对象它不会被回收

p = null // 原person对象已经没有引用,会被回收

由上面可以看出,引用计数算法是个简单有效的算法。

但它却存在一个致命的问题:循环引用。

如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。

function cycle() {
  let o1 = {}
  let o2 = {}
  o1.a = o2
  o2.a = o1
  return 'Cycle reference!'
}

cycle()

标记清除算法

现代的浏览器已经不再使用引用计数算法了。

现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。

标记清除法:

  • 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。

  • 简单来说,就是从根部(在 JS 中就是全局对象)出发定时扫描内存中的对象。

  • 凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。

根据这个概念,上面的例子可以正确被垃圾回收处理了。

参考文章:JavaScript 内存管理open in new window

17.javaScript 作用域链的理解

JavaScript 在执⾏过程中会创建一个个的可执⾏上下⽂。 (每个函数执行都会创建这么一个可执行上下文)

每个可执⾏上下⽂的词法环境中包含了对外部词法环境的引⽤,可通过该引⽤来获取外部词法环境中的变量和声明等。

这些引⽤串联起来,⼀直指向全局的词法环境,形成一个链式结构,被称为作⽤域链。

简而言之: 函数内部 可以访问到 函数外部作用域的变量, 而外部函数还可以访问到全局作用域的变量,

这样的变量作用域访问的链式结构, 被称之为作用域链

let num = 1

function fn() {
  let a = 100
  function inner() {
    console.log(a)
    console.log(num)
  }
  inner()
}
fn()

下图为由多个可执行上下文组成的调用栈:

  • 栈最底部为全局可执行上下文
  • 全局可执行上下文 之上有多个 函数可执行上下文
  • 每个可执行上下文中包含了指向外部其他可执行上下文的引用,直到 全局可执行上下文 时它指向 null

js 全局有全局可执行上下文, 每个函数调用时, 有着函数的可执行上下文, 会入 js 调用栈

每个可执行上下文, 都有者对于外部上下文词法作用域的引用, 外部上下文也有着对于再外部的上下文词法作用域的引用

=> 就形成了作用域链

18.闭包的理解

这个问题想考察的主要有两个方面:

  • 对闭包的基本概念的理解
  • 对闭包的作用的了解

什么是闭包?

MDN 的官方解释:

闭包是函数和声明该函数的词法环境的组合

更通俗一点的解释是:

内层函数, 引用外层函数上的变量, 就可以形成闭包

需求: 定义一个计数器方法, 每次执行一次函数, 就调用一次进行计数

let count = 0
function fn() {
  count++
  console.log('fn函数被调用了' + count + '次')
}
fn()

这样不好! count 定义成了全局变量, 太容易被别人修改了, 我们可以利用闭包解决

闭包实例:

function fn() {
  let count = 0

  function add() {
    count++
    console.log('fn函数被调用了' + count + '次')
  }

  return add
}
const addFn = fn()
addFn()
addFn()
addFn()

闭包的主要作用是什么?

在实际开发中,闭包最大的作用就是用来 变量私有

下面再来看一个简单示例:

function Person() {
  // 以 let 声明一个局部变量,而不是 this.name
  // this.name = 'zs'     =>  p.name
  let name = 'hm_programmer' // 数据私有

  this.getName = function () {
    return name
  }

  this.setName = function (value) {
    name = value
  }
}

// new:
// 1. 创建一个新的对象
// 2. 让构造函数的this指向这个新对象
// 3. 执行构造函数
// 4. 返回实例
const p = new Person()
console.log(p.getName()) // hm_programmer

p.setName('Tom')
console.log(p.getName()) // Tom

p.name // 访问不到 name 变量:undefined

在此示例中,变量 name 只能通过 Person 的实例方法进行访问,外部不能直接通过实例进行访问,形成了一个私有变量。