ES 基础知识点总结(一)

80 阅读15分钟

知识点梳理

  • 变量类型
    • JS的数据类型分类和判断
    • 值类型和引⽤类型
  • 原型与原型链(继承)
    • 原型和原型链定义
    • 继承写法
  • 作⽤域和闭包
    • 执⾏上下⽂
    • this
    • 闭包是什么
  • 异步
    • 同步vs异步
    • 异步和单线程
    • 前端异步的场景
  • ES6/7新标准的考查
    • 箭头函数
    • Module
    • Class
    • Set和Map
    • Promise

变量类型

JavaScript是⼀种弱类型脚本语⾔,所谓弱类型指的是定义变量时,不需要什么类型,在程序运⾏过程中会⾃动判断类型。

ECMAScript中定义了7种原始类型:BooleanStringNumberNullUndefinedSymbol(ES6新定义)

  • Boolean
  • String
  • Number
  • Null
  • Undefined
  • Symbol(ES6新定义)
  • BigInt(新增)

注意:原始类型不包含Object。

题⽬:类型判断⽤到哪些⽅法?

typeof

typeof xxx 得到的值有以下⼏种类型: undefined,boolean,number,string,object,function、symbol,⽐较简单,不再⼀⼀演示了。这⾥需要注意的有三点:

  • typeof null结果是object,实际这是 typeof 的⼀个bug,null是原始值,⾮引⽤类型
  • typeof [1, 2]结果是object,结果中没有array这⼀项,引⽤类型除了function其他的全部都是object
  • typeof Symbol()typeof获取symbol类型的值得到的是symbol,这是ES6新增的知识点

instanceof

⽤于实例和构造函数的对应。例如判断⼀个变量是否是数组,使⽤typeof⽆法判断,但可以使⽤[1,2] instanceof Array来判断。因为,[1,2]是数组,它的构造函数就是Array。同理:

function Foo(name) {
    this.name = name
}
varfoo = newFoo('bar')
console.log(fooinstanceofFoo) // true

题⽬:值类型和引⽤类型的区别

除了原始类型,ES还有引⽤类型,上⽂提到的typeof识别出来的类型中,只有objectfunction是引⽤类型,其他都是值类型。

根据JavaScript中的变量类型传递⽅式,⼜分为值类型引⽤类型,值类型变量包括Boolean、String、Number、Undefined、Null,引⽤类型包括了Object类的所有,如Date、Array、Function等。在参数传递⽅式上,值类型是按值传递,引⽤类型是按共享传递。下⾯通过⼀个⼩题⽬,来看下两者的主要区别,以及实际开发中需要注意的地⽅。

下⾯通过⼀个测试,来看下两者的主要区别,以及实际开发中需要注意的地⽅。

//值类型
var a = 10
var b = a
b = 20
console.log(a) // 10
console.log(b) // 20

上述代码中,a,b都是值类型,两者分别修改赋值,相互之间没有任何影响。再看引⽤类型的例⼦:

//引用关系
var a = {x: 10, y: 20}
var b = a
b.x = 100
b.y = 200
console.log(a) //{ x: 100, y: 200 }
console.log(b)//{ x: 100, y: 200 }

说出下⾯代码的执⾏结果,并分析其原因。

function foo(a){
    a = a * 10
}
function bar(b) {
    b.value = 'new'
}
foo(a);
bar(b);
console.log(a); // 1
console.log(b); // value: new

通过代码执⾏,会发现:

  • a的值没有发⽣改变
  • ⽽b的值发⽣了改变

这就是因为Number类型的a是按值传递的,⽽Object类型的b是按共享传递的。

JS中这种设计的原因是:按值传递的类型,复制⼀份存⼊栈内存,这类类型⼀般不占⽤太多内存,⽽且按值传递保证了其访问速度。按共享传递的类型,是复制其引⽤,⽽不是整个复制其值(C语⾔中的指针),保证过⼤的对象等不会因为不停复制内容⽽造成内存的浪费。

引⽤类型经常会在代码中按照下⾯的写法使⽤,或者说容易不知不觉中造成错误!

var obj = {
    a: 1,
    b: [1, 2, 3]
}
var a = obj.a
var b = obj.b
a = 2
b.push(4)
console.log(obj, a, b)

虽然obj本身是个引⽤类型的变量(对象),但是内部的ab⼀个是值类型⼀个是引⽤类型,a的赋值不会改变obj.a,但是b的操作却会反映到obj对象上。

原型和原型链

JavaScript是基于原型的语⾔,原型理解起来⾮常简单,但却特别重要,下⾯还是通过题⽬来理解下JavaScript的原型概念。

题⽬:如何理解JavaScript的原型

  • 所有的引⽤类型(数组、对象、函数),都具有对象特性,即可⾃由扩展属性(null除外)
  • 所有的引⽤类型(数组、对象、函数),都有⼀个__proto__属性,属性值是⼀个普通的对象
  • 所有的函数,都有⼀个prototype属性,属性值也是⼀个普通的对象所有的引⽤类型(数组、对象、函数),__proto__属性值指向它的构造函数的prototype属性值
// 要点一:自由扩展属性
var obj = {};obj.a = 100
var arr = [];arr.a = 100
function fn() {}
fn.a = 100

// 要点二: __proto__
console.log(obj.__proto__)
console.log(arr.__proto__)
console.log(fn.__proto__)

// 要点三: 函数要有prototype
console.log(fn.prototype)

// 要点四: 引用类型的__proto__属性值向它的构造函数的 prototype
console.log(obj.__proto__ === obj.prototype)

原型

先来看一个简单的代码示例

// 构造函数
function Foo(name, age) {
    this.name = name
}
Foo.prototype.alertName = function () {
    alert(this.name)
}

// 创建示例
var f = new Foo('zhangsan')
f.alertName = function () {
    console.log(this.name);
}

// 测试
f.printName()
f.alertName()

执⾏printName时很好理解,但是执⾏alertName时发⽣了什么?这⾥再记住⼀个重点当试图得到⼀个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找,因此f.alertName就会找到Foo.prototype.alertName。那么如何判断这个属性是不是对象本身的属性呢?使⽤hasOwnProperty,常⽤的地⽅是遍历⼀个对象的时候。

那么如何判断这个属性是不是对象本身的属性呢?使⽤hasOwnProperty,常⽤的地⽅是遍历⼀个对象的时候。

var item
for (item in f) {
    // 高级浏览已经在 for in 中屏蔽来自原型的属性,
    // 但是在这里建议去加上这个判断,保证程序的健壮性
    if(f.hasOwnProperty(item)) {
        console.log(item);
    }
}

题⽬:如何理解JS的原型链

原型链

还是接着上⾯的示例,如果执⾏f.toString()时,⼜发⽣了什么?

// 省略n行

// 测试
f.printName()
f.alertName()
f.toString()

因为f本身没有toString(),并且f.__proto__(即Foo.prototype)中也没有toString。这个问题还是得拿出刚才那句话——当试图得到⼀个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找。

如果在f.__proto__中没有找到toString,那么就继续去f.proto.__proto__中寻找,因为f.__proto__就是⼀个普通的对象⽽已嘛!

  • f.__proto__Foo.prototype,没有找到toString,继续往上找

  • f.__proto__.__proto__即Foo.prototype.proto。Foo.prototype就是⼀个普通的对象,因此Foo.prototype.__proto__就是Object.prototype,在这⾥可以找到toString

  • 因此f.toString最终对应到了Object.prototype.toString这样⼀直往上找,你会发现是⼀个链式的结构,所以叫做“原型链”。如果⼀直找到最上层都没有找到,那么就宣告失败,返回undefined。最上层是什么——Object.prototype.__proto__ === null

作用域链中的this

所有从原型或更⾼级原型中得到、执⾏的⽅法,其中的this在执⾏时,就指向了当前这个触发事件执⾏的对象。因此printNamealertName中的this都是f

作⽤域和闭包

题⽬:现在有个HTML⽚段,要求编写代码,点击编号为⼏的链接就alert弹出其编号

<ul>
    <li>编号1,点击我请弹出1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
 </ul>

⼀般不知道这个题⽬⽤闭包的话,会写出下⾯的代码:

var list = document.getElementsByTagName('li')
for (var i = 0; i < list.length; i++ ) {
    list[i].addEventListener('click', function () {
        alert(i + 1)
      }, true)
}

实际上执⾏才会发现始终弹出的是6,这时候就应该通过闭包来解决:

var list = document.getElementsByTagName('li')
for (var i = 0; i < list.length; i++ ) {
    list[i].addEventListener('click', function (i) {
        alert(i + 1)
      }(i), true)
}

要理解闭包,就需要我们从「执⾏上下⽂」开始讲起。

执⾏上下⽂

从⼀个关于变量提升的知识点,⾯试中可能会遇⻅下⾯的问题,很多候选⼈都回答错误:、

console.log(a); // undefined
var a = 100

fn('zhangsan') // 'zhansan' 20
function fn(name) {
    age = 20 
    console.log(name, age);
    var age
}

console.log(b); // 报错
// Uncaught ReferenceError: b is not defined
b = 100

在⼀段JS脚本(即⼀个<script>标签中)执⾏之前,要先解析代码(所以说JS是解释执⾏的脚本语⾔),解析的时候会先创建⼀个全局执⾏上下⽂环境,先把代码中即将执⾏的(内部函数的不算,因为你不知道函数何时执⾏)变量、函数声明都拿出来。变量先暂时赋值为undefined,函数则先声明好可使⽤。这⼀步做完了,然后再开始正式执⾏程序。再次强调,这是在代码执⾏之前才开始的⼯作。

我们来看下上⾯的⾯试⼩题⽬,为什么aundefined,⽽b却报错了,实际JS在代码执⾏之前,要「全⽂解析」,发现var a,知道有个a的变量,存⼊了执⾏上下⽂,⽽b没有找到var关键字,这时候没有在执⾏上下⽂提前「占位」,所以代码执⾏的时候,提前报到的a是有记录的,只不过值暂时还没有赋值,即为undefined,⽽b在执⾏上下⽂没有找到,⾃然会报错(没有找到b的引⽤)。

另外,⼀个函数在执⾏之前,也会创建⼀个函数执⾏上下⽂环境,跟全局上下⽂差不多,不过函数执⾏上下⽂中会多出thisarguments和函数的参数。

总结一下:

  • 范围:⼀段<script>、js⽂件或者⼀个函数
  • 全局上下⽂:变量定义,函数声明
  • 函数上下⽂:变量定义,函数声明,thisarguments

this

先搞明⽩⼀个很重要的概念——this的值是在执⾏的时候才能确认,定义的时候不能确认为什么呢——因为this是执⾏上下⽂环境的⼀部分,⽽执⾏上下⽂需要在代码执⾏之前确定,⽽不是定义的时候。看如下例⼦

var a = {
    name: 'A',
    fn: function () {
        console.log(this.name);
      }
}
a.fn() // this === a
a.fn.call({name:"B"}) // this === {nanme: B}
var fn1 = a.fn
fn1() // this === window

this执行会有不同,主要集中在一下场景中

  • 作为构造函数执⾏,构造函数中
  • 作为对象属性执⾏,上述代码中a.fn()
  • 作为普通函数执⾏,上述代码中fn1()
  • ⽤于call apply bind,上述代码中a.fn.call({name: 'B'})

下⾯再来看看什么是作⽤域和作⽤域链,作⽤域链和作⽤域。

题⽬:如何理解JS的作⽤域和作⽤域链

作⽤域

if(true) {
    var name = 'mint'
}
console.log(name);

从上⾯的例⼦可以体会到作⽤域的概念,作⽤域就是⼀个独⽴的地盘,让变量不会外泄、暴露出去。上⾯的name就被暴露出去了,因此,JS没有块级作⽤域,只有全局作⽤域和函数作⽤域

var a = 100
function fn() {
    var a = 200
    console.log('fn', a);
    console.log('global', a);
}

全局作⽤域就是最外层的作⽤域,如果我们写了很多⾏JS代码,变量定义都没有⽤函数包括,那么它们就全部都在全局作⽤域中。这样的坏处就是很容易撞⻋、冲突。

//张三写的代码中
var data= {a: 100}

//李四写的代码中
var data= {x: true}

这就是为何jQuery、Zepto等库的源码,所有的代码都会放在(function(){....})()中。因为放在⾥⾯的所有变量,都不会被外泄和暴露,不会污染到外⾯,不会对其他的库或者JS脚本造成影响。这是函数作⽤域的⼀个体现。附:ES6中开始加⼊了块级作⽤域,使⽤let定义变量即可,如下:

if (true) {
    letname='zhangsan'
}
console.log(name)//报错,因为let定义的name是在if这个块级作用域

作⽤域链

⾸先认识⼀下什么叫做⾃由变量。如下代码中,console.log(a)要得到a变量,但是在当前的作⽤域中没有定义a(可对⽐⼀下b)。当前作⽤域没有定义的变量,这成为⾃由变量。⾃由变量如何得到——向⽗级作⽤域寻找。

var a = 100
function fn() {
    var a = 200
    console.log(a);
    console.log(b);
}
fn()

如果⽗级也没呢?再⼀层⼀层向上寻找,直到找到全局作⽤域还是没找到,就宣布放弃。这种⼀层⼀层的关系,就是作⽤域链

var a = 100
function foo() {
    var b = 200
    function bar() {
        var c = 300
        console.log(a);// 自由变量,顺作用域链想父作用域找
        console.log(b);// 自由变量,顺作用域链想父作用域找
        console.log(c);// 本作用域的变量
    }
    bar()
}
foo()

闭包

再来看一个例子,来理解闭包

function Foo() {
    var a = 100
    return function () {
        console.log(a);
    }
}

var foo = Foo()
var a = 200
foo()

⾃由变量将从作⽤域链中去寻找,但是依据的是函数定义时的作⽤域链,⽽不是函数执⾏时,以上这个例⼦就是闭包。闭包主要有两个应⽤场景:

  • 函数作为返回值,上⾯的例⼦就是
  • 函数作为参数传递,看以下例⼦
function Foo() {
    var a = 100
    return function () {
        console.log(a);
    }
}
function Bar(foo) {
    var a = 200
    console.log(foo());
}

var foo = Foo()
Bar(foo)

异步

同步vs异步

先看下⾯的demo,根据程序阅读起来表达的意思,应该是先打印100,1秒钟之后打印200,最后打印300。但是实际运⾏根本不是那么回事。

console.log(100);
setTimeout(function () {
    console.log(200);
}, 1000)
console.log(300);

再对⽐以下程序。先打印100,再弹出200(等待⽤户确认),最后打印300。这个运⾏效果就符合预期要求。

console.log(100);
alert(200); // 1秒钟之后点击确认
console.log(300);

这俩到底有何区别?——第⼀个示例中间的步骤根本没有阻塞接下来程序的运⾏,⽽第⼆个示例却阻塞了后⾯程序的运⾏。前⾯这种表现就叫做异步(后⾯这个叫做同步),即不会阻塞后⾯程序的运⾏

异步和单线程

JS需要异步的根本原因是JS是单线程运⾏的,即在同⼀时间只能做⼀件事,不能“⼀⼼⼆⽤”。

⼀个Ajax请求由于⽹络⽐较慢,请求需要5秒钟。如果是同步,这5秒钟⻚⾯就卡死在这⾥啥也⼲不了了。异步的话,就好很多了,5秒等待就等待了,其他事情不耽误做,⾄于那5秒钟等待是⽹速太慢,不是因为JS的原因。

讲到单线程,我们再来看个真题:

题⽬:讲解下⾯代码的执⾏过程和结果

var a = true
setTimeout(function () {
    a = false
  }, 100)
  while(a) {
      console.log('while执行了');
  }

这是⼀个很有迷惑性的题⽬,不少候选⼈认为100ms之后,由于a变成了false,所以while就中⽌了,实际不是这样,因为JS是单线程的,所以进⼊while循环之后,没有「时间」(线程)去跑定时器了,所以这个代码跑起来是个死循环!

前端异步的场景

  • 定时setTimeout setInterval
  • ⽹络请求,如Ajax<img>加载

Ajax代码示例

console.log('start');
$.get('./data.json', function (data) {
    console.log(data);
})

img代码示例(常⽤于打点统计)

console.log('start');
var img =- document.createElement('img')
// 或者 img = new Image()
img.onload = function () {
    console.log('loaded');
    img.onload = null
  }
img.src = './xxx.jpg'
console.log('end');

ES6/7新标准的考查

题⽬:ES6箭头函数中的this和普通函数中的有什么不同

箭头函数

箭头函数是ES6中新的函数定义形式,function name(arg1, arg2) {...}可以使⽤(arg1,arg2) => {...}来定义。示例如下:

// JS 普通函数
var arr = [1, 2, 3]
arr.map(function(item) {
    console.log(index);
    return item + 1
})

// ES6 箭头函数
const arr2 = [1, 2, 3]
arr.map((item, index) => {
    console.log(index);
    return item + 1
})

箭头函数存在的意义,第⼀写起来更加简洁,第⼆可以解决ES6之前函数执⾏中this是全局变量的问题,看如下代码

function fn() {
    console.log('real', this); // {a: 100}, 该作用域下的 this 的真实值
    var arr = [1, 2, 3]
        // 普通JS   
    arr.map(function(item) {
        console.log('js', this); // window 普通函数, 这里打印出来的全局变量,令人费解
        return item + 1
    })

    // 箭头函数
    arr.map((item, inedx) => {
        console.log('es6', this); // {a: 100} 箭头函数,这里打印出来的就是父作用域的this
        return item + 1
    })
}

fn.call({ a: 100 })

题⽬:ES6模块化如何使⽤?

Module

ES6中模块化语法更加简洁,直接看示例。如果只是输出⼀个唯⼀的对象,使⽤export default即可,代码如下:

// 创建 util1.js ⽂件,内容如
export  default {
    a: 100
}
    
// 创建 index.js 文件,内容如
import obj from './until1.js'

console.log(obj)

如果想要输出许多个对象,就不能⽤default了,且import时候要加{...},代码如下:

// 创建 util2.js文件,内容如
export function fn1() {
    alert('fn1')
}

export function fn2() {
    alert('fn2')
}

// 创建 index.js 文件,内容如
import { fn1, fn2 } from './util2.js'
fn1()
fn2()

题⽬:ES6 class和普通构造函数的区别

class

class其实⼀直是JS的关键字(保留字),但是⼀直没有正式使⽤,直到ES6。ES6的class就是取代之前构造函数初始化对象的形式,从语法上更加符合⾯向对象的写法。例如:

JS构造函数的写法:

function MathHandle(x, y) {
    this.x = x;
    this.y = y;
}

MathHandle.prototype.add = function () {
    return this.x + this.y;
}

var m = new MathHandle(1, 2)
console.log(m.add())

ES6 class的写法:

class MathHandle {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    
    add() {
        return this.x + this.y;
    }
}

const m = new MathHandle(1, 2);
console.log(m.add());

注意以下⼏点,全都是关于class语法的:

  • class是⼀种新的语法形式,是class Name {...}这种形式,和函数的写法完全不⼀样
  • 两者对⽐,构造函数函数体的内容要放在class中的constructor函数中,constructor即构造器,初始化实例时默认执⾏
  • class中函数的写法是add(){...}这种形式,并没有function关键字

使⽤class来实现继承就更加简单了,⾄少⽐构造函数实现继承简单很多。

JS构造函数实现继承:

// 动物
function Animal() {
    this.eat = function () {
        console.log('animal eat')
    }
}

// 狗
function Dog() {
    this.bark = function () {
        console.log('dog bark')
    }
}
Dog.prototype = new Animal()
// 哈士奇
const hashiqi = new Dog()

ES6 class实现继承:

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    eat() {
        console.log(`${this.name} eat`)
    }
}

class Dog extends Animal {
    constructor(name) {
        super(name);
        this.name = name
    }
    say() {
        console.log(`${this.name} say`)
    }
}

const dog = new Dog('哈士奇')
dog.say()
dog.eat()

注意以下两点:

  • 使⽤extends即可实现继承,更加符合经典⾯向对象语⾔的写法,如Java
  • ⼦类的constructor⼀定要执⾏super(),以调⽤⽗类的constructor

题⽬:ES6中新增的数据类型有哪些?

Set和 Map

SetMap都是ES6中新增的数据结构,是对当前JS数组和对象这两种重要数据结构的扩展。由于是新增的数据结构,总结⼀下两者最关键的地⽅:

  • Set类似于数组,但数组可以允许元素重复,Set不允许元素重复
  • Map类似于对象,但普通对象key必须是字符串或者数字,⽽Map的key可以是任何数据类型

Set

Set类似于数组,但数组可以允许元素重复,Set不允许元素重复Map类似于对象,但普通对象的key必须是字符串或者数字,⽽Map的key可以是任何数据类型

// 例1
const set = new Set([1,2,3,3,4])
console.log(set);// Set { 1, 2, 3, 4 }

//例2
const set2 = new Set();
[2, 3, 5, 4, 5, 8, 8].forEach(item => set2.add(item))
for(let item of set2) {
    console.log(item);
}
// 2 3 5 4 8

Set实例的属性和⽅法有:

  • size:获取元素数量。
  • add(value):添加元素,返回Set实例本身。
  • delete(value):删除元素,返回⼀个布尔值,表示删除是否成功。
  • has(value):返回⼀个布尔值,表示该值是否是Set实例的元素。
  • clear():清除所有元素,没有返回值。
const s = new Set()
s.add(1).add(2).add(2); //添加元素

s.size; // 2

s.has(1); //true
s.has(2); //true
s.has(3); //false

s.delete(2);
s.has(2);//false

s.clear();
console.log(s); // Set(0) {}

Set实例的遍历,可使⽤如下⽅法:

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。不过由于Set结构没有键名,只有键值(或者说键名和键值是同⼀个值),所以keys()和values()返回结果⼀致。
  • entries():返回键值对的遍历器。
  • forEach():使⽤回调函数遍历每个成员。
let set = new Set(['aaa','bbb','ccc'])

for (let item of set.keys()) {
    console.log(item);
}
// aaa
// bbb
// ccc

for (let item of set.values()) {
    console.log(item);
}
// aaa
// bbb
// ccc

for (let item of set.entries()) {
    console.log(item);
}
// [ 'aaa', 'aaa' ]
// [ 'bbb', 'bbb' ]
// [ 'ccc', 'ccc' ]

set.forEach((value, key) => console.log(key + ':' + value))
// aaa:aaa
// bbb:bbb
// ccc:ccc

Map

Map的⽤法和普通对象基本⼀致,先看⼀下它能⽤⾮字符串或者数字作为key的特性。

const map = new Map();
const obj = {p: 'hello world'};

map.set(obj, 'OK'); //OK
console.log(map.get(obj));

console.log(map.has(obj)); //true
console.log(map.delete(obj)); // true
console.log(map.has(obj)); // false

需要使⽤new Map()初始化⼀个实例,下⾯代码中set get has delete顾名即可思义(下⽂也会演示)。其中,map.set(obj, 'OK')就是⽤对象作为的key(不光可以是对象,任何数据类型都可以),并且后⾯通过map.get(obj)正确获取了。

Map实例的属性和⽅法如下:

  • size:获取成员的数量
  • set:设置成员key和value
  • get:获取成员属性值
  • has:判断成员是否存在
  • delete:删除成员clear:清空所有
const map = new Map()
map.set('aaa', 100)
map.set('bbb', 200)

for (let key of map.keys()) {
    console.log(key)
}
// "aaa"
// "bbb"

for (let value of map.values()) {
    console.log(value)
}
// 100
// 200

for (let item of map.entries()) {
    console.log(item[0], item[1])
}
// aaa 100
// bbb 200

// 或者
for (let [key, value] of map.entries()) {
    console.log(key, value)
}
// aaa 100
// bbb 200

Promise

PromiseCommonJS提出来的这⼀种规范,有多个版本,在ES6当中已经纳⼊规范,原⽣⽀持Promise对象,⾮ES6环境可以⽤类似Bluebird、Q这类库来⽀持。

Promise可以将回调变成链式调⽤写法,流程更加清晰,代码更加优雅。

简单归纳下Promise:三个状态两个过程⼀个⽅法,快速记忆⽅法:3-2-1

三个状态:pendingfulfilledrejected 两个过程:

  • pending→fulfilled(resolve)
  • pending→rejected(reject) ⼀个⽅法:then 有其他概念,如catchPromise.all/race