04-Javascript核心解密

138 阅读28分钟

Javascript核心解密(ES6)

这节课的内容有一些多,但是又很重要,建议大家多抽出时间来学习

讲在课程之前

在你学习这部分内容时,我希望你已经学习😙 :

  1. JS的6个数据类型
  2. 数组的常用方法
  3. 选择,循环结构以及函数的相关知识

如果已经正确的按照我们的课件顺序进行学习,那么想必潘神的课件已经让大家打好了一个相当坚实的基础( ̄▽ ̄)",如果还没有,那么请你跳转至:juejin.cn/post/728157…

潘神的课件捏!🆒

当然我讲的内容有部分也会和他重叠,如果有发现重复的地方觉得自己掌握的不错可以自行跳过,不过我应该会讲细致一些

关于ES6是什么呢?

在上一节课中,我们已经知道 😳 :

JS的组成主要分为三部分:ECMAScript(核心),DOM(文档对象模型),BOM(浏览器对象模型)。

其中ECMAScript的缩写就是ES,因此我们的ES6其实指的就是它的一个版本(不记得的可以回顾一下)。在互联网的各类教程中,我们往往会将ES5之后的内容统称为ES6,也就是说ES6其实很多时候代指ES6,7,8,9.....!

而ES是我们脚本语言的标准,也就是一套设计规则,它并不是我们语言的具体实现,所以JS和ES的关系就是:

JS实现了ES,ES规定了JS

这里的 实现 二字,或许有些同学不是很理解,那么我这样说吧

我定义这样一类事物:

1.它吃起来的味道是甜的或者酸的

2.它可以是黄色,红色,橙色,绿色

3.它可以是树上长的

这样的事物,我给它取名为水果1号

那么满足这些规则的,显然可以有苹果,香蕉等等

那么在这个简单的例子中,苹果和香蕉就是满足,或者说实现我所说的规范的一些具体的事物,这样具体的事物我们往往就叫它 对象 ,而定义的规范的集合,其实是一个抽象的事物,我们只是规定了它的一些特征,它实际上可以是很多具体的东西,我们就把这个抽象的事物叫做一个 。(所以就是对象满足了类所规定的一些性质或特征,我们就说某个对象实现了类,这个实现的过程我们叫他 实例化 )

我们有时也会把我们通过类创造出来的对象称为实例,但是关于实例和对象,大家在工作中不必要分的很清楚,感兴趣的小伙伴可以自行了解😁

这其实就是面向对象编程的一个基本思想,对象是我们对客观事物的一个抽象(定量的看待事物),类是对对象的一个抽象(定性看待事物)(这句话不懂没事,上面看懂就行)。所以现在大家可以理解JS和ES的关系了吗?=v=

当然这只是一个简单的例子,可能对这个概念的解释并不是特别的深入,但我希望大家能从中感受这种思想,因为我们在很多地方都会用到这个思想。如果你有更好的理解当然是最好的拉(●ˇ∀ˇ●)

如果你听说过面向过程?(不感兴趣可以跳过这里) 在这里给一个小小的解释,大家看看就好:

面向过程(Procedure Oriented 简称PO :如C语言):

从名字可以看出它是注重过程的。当解决一个问题的时候,面向过程会把事情拆分成: 一个个函数和数据(用于方法的参数) 。然后按照一定的顺序,执行完这些方法(每个方法看作一个过程),等方法执行完了,事情就搞定了。

面向对象(Object Oriented简称OO :如C++,JAVA等语言):

看名字它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。

现在开始

1.数据类型

我们在之前的课程里面应该已经接触到了:

  • 基本数据类型(值类型):字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol;
  • 引用数据类型:对象(Object)、数组(Array)、函数(Function)

那么引用数据类型和基本数据类型的区别是什么呢?😕

从字面意义来说,引用数据类型其实就是这个数据它本身是一个引用,而非值本身,值类型则保存的就是这个值本体。

如果说你还没有学习过一门编程语言,那么你可能会有一个疑问,引用是什么?

如果你已经学习C语言,可以尝试看到C语言指针相关的内容,再去搜索引用是什么,网上有很多的文章可以看

让我们对引用做一个简单的解释:

一个变量的存储,我们应该从一定的物理意义上进行思考,它不只是我们写下的一行代码而已,在计算机中,它被存储在内存当中,而每个变量的数据量不一样,所以我们就需要定义一个内存的基本单位,即字节(Byte)

所以内存就被分成了一个一个小块,每个小块是一个字节,数据就被分成几个字节来进行存储,这里不展开讲,感兴趣的同学可以自行学习内存的相关内容

在分成一个个小块后,我们要怎么样找到我们存入的每个变量呢??😟如果只是这样电脑可不知道哪个变量谁是谁,所以我们是不是应该给每一个小块进行编号啊,再分别让他们记住自己的位置

因此内存地址就诞生了,用来给数据的存储提供索引,往往都是0x开头接上一串数字,来代表每个字节的位置,顺序排布😯

内存模型.bmp

上图这种小格子,就是我们理想化的内存空间模型,而我们定义的变量的数据,就会存在这些格子里

上图中,整型变量a直接保存了数值10,它的地址是0x00010

可以发现指针变量p的定义方式是指向a,存储的是一个地址而非数值,即a的地址0x00010,取p实际的数值其实就是10,只不过我们需要先访问p,获得它存储的地址,也就是获得a的位置,再去获取a存储的值,即10。

当然这些是C语言中的内容,先理解这个小例子,我们类比成 Javascript再看看🙌

来看一个小例子(这里的let就先看成var,后面会讲):

let a = 'lanshan';
let b = {
  name: '灿灿',
  age: 18
}
​
let temp = a;
temp = 'frontend';
console.log(temp); // 'frontend'
console.log(a); // 'lanshan'let c = b;
console.log(c);
// 打印:
// {
//   name: '灿灿',
//   age: 18
// }
c.male = '男';
console.log(c);
console.log(b);
// 20、21行都打印:
// {
//   name: '灿灿',
//   age: 18,
//   male: '男'
// }

从上例我们可以看到:

基本数据类型的变量保存的是原始值,他代表的值就是我们赋予的原始值;

引用数据类型的变量保存的是引用值(在栈内存中),该引用值是指向堆内存空间的一个地址,而不是对象本身。😎

故上例我们将b赋值给c的时候,是将b变量存储的地址赋值给了c变量,从而调用c为对象添加属性,实际上也是为b所指向的对象添加属性。😎

栈内存和堆内存具体是什么可以先不用管,就理解成存在于内存中的两片区域

内存.png

所以基本数据类型在内存中分配一个固定的空间,而引用数据类型在内存中分配一个地址,实际数据存储在另外的位置。

有的人可能会有疑问,如果基本数据类型的变量本身存我们需要的值,那么它自己所在的地址怎么被程序找到呢,引用数据类型存储的也不是自己真实地址,而是自己的值的地址

关于这个问题,其实每个变量名都是一个“记号”,程序会根据你定义的变量的名字给对应的变量分配一个地址(它是记得自己做的事情的),所以在程序编译或运行起来的时候,它会自己去根据变量名去寻找对应的地址,就可以确定你要的那个变量

2.变量声明

在介绍了引用数据类型后,我们来讲讲新的变量声明方式,分别是const和let,简单来说:

const用来声明常量,let用来声明变量

常量就是不能被改变值的变量,但是需要注意一点👿:

const arr = [0, 1, 2, 3];
const len = arr.push(4); // 数组末尾添加一个4,并返回数组添加后的长度
console.log(arr, len) // [0,1,2,3,4] 5
​
const obj = {
  name: '灿灿',
  age: 19
}
obj.male='男'; // 新设置性别的属性
console.log(obj); // 打印结果如下图所示

可以发现,在使用const定义数组arr后,它里面的值依然被改变了,这是因为arr是一个引用数据类型,它存储的数据实际上是一个内存地址,我们修改它的具体数据其实是可以的,但不能修改它的地址,因此当我们声明一个引用数据类型的时候,就应该使用const,可以避免它的地址被修改。💆

有了这两个方式之后我们就尽量不要再使用var啦

三种变量的使用优先级:

  • 不使用 var(容易污染全局作用域)
  • const 优先,let 次之

想要深入学习关于var和const,let的区别的同学可以看看 谭神之前的课件 关于这部分的说明(作用域,执行上下文)

或者自行百度,有大量文章 👊

看到这里了,建议复习一下数组常用方法:

增删改,循环和筛选,合并,排序,转字符串 这些也可以看看 谭神之前的课件

其实ES6还有各类方法的拓展,可以等大家学习完后面的内容再来打这部分的基础

3.新增语法

解构赋值

  • 解构赋值可以让我们将数组中的值或对象的属性按照顺序取出,赋值给其他变量。🐑

两个简单的例子:

//对数组进行解构赋值
const x = [1, 2, 3, 4, 5];
const [y, z] = x;
console.log(y); // 1
console.log(z); // 2
//对 对象进行解构赋值
const obj = { a: 1, b: 2 };
const { a, b } = obj;
// 相当于
// const a = obj.a;
// const b = obj.b;
// 其实就是把我们要的数据放在等式左边依次排列,将他们按顺序取出来

两个复杂一些的例子:

//
//      数组的嵌套解构
//
let [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo) // 1
console.log(bar) // 2
console.log(baz) // 3
​
let [ , , third] = ["重邮", "蓝山", "工作室"];
console.log(third) // "工作室"
​
let [x, , y] = [1, 2, 3];
console.log(x) // 1
console.log(y) // 3
​
let [head, ...tail] = [1, 2, 3, 4];
console.log(head) // 1
console.log(tail) // [2, 3, 4]//
//      对象的嵌套解构
//
let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};
​
let { p: [x, { y }] } = obj;
console.log(x) // "Hello"
console.log(y) // "World"

在我们进行解构赋值的时候,也可以给某些数据设定默认值,此时如果我们不给它传值,它就会是默认值

//  数组
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b', z] = ['a', , '工作室'] // x='a', y='b', z='工作室'
//  对象
let {x, y = 5} = {x: 1};//  x=1  y=5

模板字符串

  • 模板字符串,就是通过替换占位符来进行字符串插值,以得到我们想要的字符串

基本写法: 第一段文字{变量名or表达式}第二段文字 (注意是用反引号对``包裹的,中间使用花括号{})

学过C语言的小伙伴应该懂这种感觉了,其实就是格式化输出对吧🐔

看个小例子:

const a = 5;
const b = 10;
// 注意反引号对 `` 包裹
console.log(`Fifteen is ${a + b} and
not ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."

简单来说就是,我们在原来的一段完整的字符串中,挖出来一个 “插槽” ,然后填入我们想要的——可能是表达式,也可能是变量

这可以给我们的字符串提供极大的自由度,因为里面的字符内容是一个可以变化的东西了

箭头函数

  • 箭头函数其实就是函数,因为酷似箭头得名,是函数的一种新的写法

箭头函数相当于一个匿名函数,并且简化了函数定义。🙏

基本写法: ( 参数... ) => { 函数体 }

括号内表达传入的参数,如果只有一个参数,括号可以省略,如果有多个参数或没有参数,则不能省略。

箭头后面为函数体,用大括号包裹。如果只含返回值,大括号可以省略。

let f = () => 5;
// 等同于
let f = function () { return 5 };
​
let sum = (num1, num2) => num1 + num2;
// 等同于
let sum = function(num1, num2) {
  return num1 + num2;
};
​
let foo = (name) => {
    if(name === "蓝山工作室"){
        return "是蓝山工作室"
    }
    return '不是蓝山工作室'
}
foo('蓝山工作室') // 是蓝山工作室

扩展运算符

  • 扩展运算符 ... 能够将 数组 转换为逗号分隔的 参数序列,或者说可以讲数组均匀“拆散”🐶

可用于

数组的合并和克隆
//数组的合并
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];
const arr3 = [...arr1, ...arr2]; // ['a', 'b', 'c', 'd']
//数组的克隆
const arr1 = ['a', 'b'];
const arr2 = [...arr1]; // ['a', 'b']

更多的用法等大家学习后面的知识发现

ES6 新增引用数据类型:Symbol,Set,Map😐

不是特别重要,我们拿Set来介绍,另外两个感兴趣的同学可以自行了解即可

放在这里是因为例子使用了扩展运算符

它类似于数组,但其成员的值都是唯一的,集合实现了 iterator 接口(迭代器,简单来说就是内置了迭代器的对象就可以用统一的for..of方法进行遍历),所以可以使用 扩展运算符for...of... 进行遍历

集合Set的属性和方法🐕

  1. size 返回集合的元素个数
  2. add 增加一个新元素,返回当前集合
  3. delete 删除元素,返回 boolean
  4. has 检测集合中是否包含某个元素,返回 boolean
  5. clear 清空集合

可以用于数组去重

let arr = [1,2,3,4,5,4,3,2,1]
let result = [...new Set(arr)] //代表以arr作为生成集合的数据,并将new返回的Set使用扩展运算符展开
console.log(result) //[1,2,3,4,5]

通过使用数组的 filter 方法,还可以取交集和并集,不展开说明了

rest参数

在ES5中,我们可以使用arguments对象获取参数,arguments是一个对应于传递给函数的所有参数的类数组对象🐌

arguments有长度length属性,并且属性的索引是从零开始的,但它并不是一个数组,而是类数组对象(不必深究此内容)

function func1(a, b, c) {
  console.log(arguments.length) // 3
  console.log(arguments[0]);
  // Expected output: 1
​
  console.log(arguments[1]);
  // Expected output: 2
​
  console.log(arguments[2]);
  // Expected output: 3
}
​
func1(1, 2, 3);

ES6 中引入了 rest 参数,用于获取函数的多余参数(代替了arguments)。

基本形式为: (...变量名) ,放在扩展运算符这里是为了让大家方便记忆

const add = (...nums) => {
  let sum = 0;
  for (let i of nums) {
    sum += i;
  }
  return sum;
}
​
console.log(add(2, 5, 3, 4)) // 14
const add = (a, b, ...nums) => {
  let sum = 0;
  for (let i of nums) {
    sum += i;
  }
  return sum;
}
​
console.log(add(2, 5, 3, 4)) // 7

但是它肯定不是扩展运算符,看过上面的代码应该明白了

两者的区别🐸:

  • arguments 对象不是数组,而是一个类似数组的对象,所以为了使用数组的方法,必须使用 Array.from 先将其转为数组
  • rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中,在函数体中我们可以从数组里再取出相关参数
  • rest 参数必须要放到参数最后
  • rest 参数就是一个真正的数组,数组特有的方法都可以使用

在能使用ES6语法的时候,显然我们应该使用rest剩余参数🐧

4.this上下文

先来了解执行上下文和作用域:

JavaScript代码在执行时,会进入一个执行上下文中。执行上下文可以理解为当前代码的运行环境。

当代码在JavaScript中运行时,执行代码的环境非常重要,可以分为以下三类:

  • 全局环境:代码运行起来后会首先进入全局环境。
  • 函数环境:当函数被调用执行时,会进入当前函数中执行代码。
  • 块级环境:当进入块级作用域时,就会进入块级环境。

JavaScript 引擎会以栈的方式来处理它们,这个栈就是之前提到过的函数调用栈,其规定了JavaScript代码的执行顺序。栈底永远是全局上下文,栈顶是当前正在执行的上下文。

let color = 'blue'
​
const changeColor = () => {
  let anotherColor = 'red'
​
  const swapColors = () => {
    [color, anotherColor] = [anotherColor, color]
    console.log(color, anotherColor) // red blue
  }
​
  const setColor = () => {
          color = 'yellow'
    console.log(color)
  }
​
  swapColors()
  setColor()
}
changeColor()

执行上下文.png

我们用ECStack(Execution Context Stack,执行上下文堆栈)来表示处理执行上下文的堆栈。

  1. 第一步,全局上下文(Global Context)入栈,并一直处于栈底。
  2. 第二步,从可执行代码开始执行,直到遇到changeColor( ),从而创建changeColor自己的执行上下文,changeColor EC入栈。
  3. 第三步,changeColors EC入栈后,开始执行changeColors内可执行的代码。遇到swapColors( )这句代码后激活swapColors( ),swapColors EC入栈。
  4. 第四步,执行swapColors内可执行的代码,其中没有能生成其他能生成执行上下文的情况,因此这段代码顺利执行完毕 🎉,swapColors的执行上下文从栈中弹出。
  5. 第五步,继续执行之前changeColor内的可执行代码,遇到setColor( ),生产setColor的执行上下文,setColor EC入栈。
  6. 第六步,执行setColor内可执行的代码,其中没有能生成其他能生成执行上下文的情况,因此这段代码顺利执行完毕 🎉,setColors的执行上下文从栈中弹出。
  7. 第七步,继续执行之前changeColor的可执行代码,没有再遇到其他执行上下文,顺利执行完毕后弹出。这样ECStack中就只剩全局上下文了。
  8. 最后,全局上下文在浏览器窗口关闭后出栈。

作用域

常见的作用域有三种,分别是全局作用域、函数作用域、块级作用域,与我们的执行上下文是对应的。

全局作用域:我们写在最外面一层的变量与函数,全局作用域中的变量与函数可以在代码的任何地方被访问。

函数作用域:函数作用域中声明的变量和方法,只能被下层子作用域访问,而不能被其他不相干的作用域访问。

块级作用域: 在ES6之前,没有块级作用域,作用域范围为大括号内的区域。(理解:其实函数和全局,也都是一个 “块” 对吧 被{}包裹)

1.什么是this上下文?

如它的字面意思一样,它是一个代指,就和我们做阅读时候的this,that是一样的,需要根据上下文环境确定this指定的是谁

我们想知道this是什么,那就来打印一下试试:

console.log(this)
// Window {...很多内容...}
function fun () {
    console.log(this); // 还是Window对象
}
fun()

首先根据上面我们对于作用域和执行上下文的理解,this上下文应该也是和他们有很多关系的对吧,可以想想这里的输出结果代表什么

这里的window对象结果显然就是一个上下文的具体表现,说起来可能很怪?别急

我们说上下文,作用域,听起来是不是总是一个环境,一片区域的感觉

但是我们可以输出一片区域或环境吗?显然不方便,这样的作法也不聪明

所以我们的JS提供了一个对象,来代表这里的上下文,Window对象其实就是代表全局作用域的一个对象。

window对象扮演着浏览器中的全局对象的角色,因此所有在全局作用域中声明的变量,函数都会变成window对象的属性和方法。全局函数就是全局对象的方法。

一般来说,我们最常用的还是会在函数中使用this,毕竟程序主要的功能都是通过函数来实现的

再来看一个例子:

const name = '蓝山'
const robot = {
  name: '蓝妹',
  sayName () {
    console.log(this) // {name: '蓝妹', sayName:function } 其实就是robot对象
    console.log(`我是${this.name}!`)// 我是蓝妹!
  }
}
robot.sayName()

看来,this能记住自己在的环境,具体的体现就是,this会指向调用它的那个对象

但是千万不要有this是因为写在这个块里面所以就指向robot对象的错觉,真正决定this指向的,是谁来调用它,

正如我所说的“this能记住自己在的环境”,其实是指this被使用的那个环境

我们接着往下看会有更深的理解

2.箭头函数中的this

值得一提的是:

箭头函数并没有自己的this,它只会从自己的作用域链的上一层继承 this

箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。

实例如下,首先我们用普通函数的方式来实现如下代码:

let obj = {
  hp: 555,
  sayHp: function () {
    console.log(this.hp)
  }
}
​
obj.sayHp() // 555

如果我们使用箭头函数的方法来实现:

let obj = {
  hp: 555,
  sayHp: () => {
    console.log(this.hp)
  }
}
​
obj.sayHp() // undefined

会发现打印出来为undefined

let obj = {
  hp: 555,
  that: this, // window对象
  sayHp: () => {
    console.log(this.hp) 
    console.log(this) 
  }
}
console.log(obj.that) // window对象
obj.sayHp() // undefined window对象
3.call,apply,bind方法

这里给的说明可能不是很详细,感到困惑可以看看w3s给的说明 : www.w3school.com.cn/js/js_funct…

callapplybind都是改变this指向的函数的方法。或者说它可以让你用指定的参数对象来调用方法。

call

call可以立即调用并执行函数

function fun () {
    console.log('蓝山工作室')
}
fun.call() // 蓝山工作室

call可以改变函数中this的指向:

const cat = {
  name: '黛玉'
}
​
const dog = {
  name: '芬达',
  sayName () {
    console.log(`我是${this.name}`)
  }
}
​
dog.sayName() // 我是芬达
dog.sayName.call(cat) // 我是黛玉

③考虑到传参的情况:

call的第一个参数是改变this指向的对象,从第二个参数开始,之后的参数作为函数调用的参数传递。

const dog = {
  name: '芬达',
  eat (food) {
    console.log(`我是${this.name},我要吃${food}`)
  }
}
​
const cat = {
  name: '黛玉'
}
​
dog.eat('骨头') // 我是芬达,我要吃骨头
dog.eat.call(cat, '鱼') // 我是黛玉,我要吃鱼

apply

callapply的区别就在于传参的方式不同:

const dog = {
  name: '芬达',
  eat (food, hobby) {
    console.log(`我是${this.name},我要吃${food},喜欢${hobby}`)
  }
}
​
const cat = {
  name: '黛玉'
}
​
dog.eat.apply(cat, ['鱼', '睡觉']) // 我是黛玉,我要吃鱼,喜欢睡觉(以数组方式传递参数)

可以想一想,用数组进行传递有什么意义呢?很显然,如果我们使用数组这种数据类型的话,就可以调用它的各种方法便于我们的操作,这在很多情况下都会有助于简化我们的代码👯

bind

bind不会立即执行函数,bind会返回一个已经改变好this指向的函数,方便我们在后面自行调用执行。

const dog = {
  name: '芬达',
  eat (food, hobby) {
    console.log(`我是${this.name},我要吃${food},喜欢${hobby}`)
  }
}
​
const cat = {
  name: '黛玉'
}
​
const fun = dog.eat.bind(cat, '鱼', '睡觉') // 调试可以发现,到这一句时什么都没打印
fun() // 我是黛玉,我要吃鱼,喜欢睡觉

5.构造函数,原型和类

首先,请你回顾一下开头我们讲到的对象和类的关系内容

在典型的面向对象语言中(Java),都存在类的概念。类就是对象的模板,对象就是类的实例,但在 ES6之前, JS 中并没用引入类的概念。

在 ES6之前 ,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。

简单来说,ES6之前我们通过构造函数来实现面向对象,而在ES6之后我们也有了类的概念,转为用类来实现

构造函数是什么?

构造函数就是一个函数,当我们使用一个函数来创建对象的时候,这个函数就是构造函数,一般配合new来调用

比如:之前大家可能用过构造函数new Object()创建过对象,或者使用过new Array()new Date()

function Fun () {
    console.log('我是构造函数');
}
​
let temp = new Fun();
console.log(temp);
// 输出结果:
// 我是构造函数
// Fun{}

在这个例子中生成的实例就是temp,通过打印可以判断出类型为一个对象。那么我们首先可以确定的是new关键字执行了Fun函数内的语句,并返回了一个对象。那我们就可以清楚构造函数就是用来创建对象的。

如果只是创建一个空对象肯定就没有什么意思,我们可以让他丰富一些:

// 比如我需要定义一个用于存储用户数据的对象
// 我们先通过构造函数定义好需要的几个数据,根据需求可以知道他们对应是什么类型的
function User(name,id,password) {
  this.username = name;  // 用户名
  this.userid = id;      // 一个独一无二的辨识id
  this.password = password;             // 用户的密码
}
let userData = new User('abc',114514,123456);// 传入对应的数据
console.log(userData);
​
// 输出结果:
// User {username:abc,userid:114514,password:123456}

image.png

相信你从这个例子中更能体会到,类是对象的模版,我们这里的构造函数是不是就是定义了我们要的数据的模版?

总结:构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。

在 JS 中,使用构造函数时要注意以下两点:

  1. 构造函数用于创建某一类对象,其首字母要大写
  2. 构造函数要和 new 一起使用才有意义

new 在执行时会做四件事情:

  ① 在内存中创建一个新的空对象。

  ② 让 this 指向这个新的对象。

  ③ 执行构造函数里面的代码,给这个新对象添加属性和方法。

  ④ 返回这个新对象(所以构造函数里面不需要 return )。

JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的 this 上添加。通过这两种方式添加的成员,就分别称为静态成员实例成员

  • 静态成员:在构造函数本体上添加的成员称为静态成员,只能由构造函数本身来访问
  • 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问
function Star(name, age) {
    // 在开始执行时已经创建了一个空对象,并将this指向它
    this.name = name;
    this.age = age;
    this.sing = function(){
        console.log('sing')
    }
}
// 实例成员就是构造函数内部通过 this 添加的成员 name age sing 就是实例成员。实例成员只能通过实例化的对象来访问
let ldh = new Star('刘德华', 18);
ldh.sing();
​
// 静态成员 在构造函数本身上添加的成员 gender 就是静态成员
Star.gender = 'male';
console.log(Star.gender); // 静态成员只能通过构造函数来访问
console.log(ldh.gender); // 不能通过对象来访问

静态成员是构造函数本体上固有的属性,而实例成员则是实例对象上的属性,也就是通过我们定义构造函数,再用new调用传值进去的那些值

构造函数的存在问题

function Star(uname, age) {
    this.uname = uname;
    this.age = age;
    this.sing = function() {
        console.log('我会唱歌');
    }
}
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 19);
console.log(ldh.sing === zxy.sing);   // false

对于简单的数据类型,我们直接赋值就可以。

但对于复杂数据类型,当创建 ldh 这个实例对象的时候,会单独的开辟一块儿空间来存放复杂数据类型 sing 这个方法,创建 zxy 对象的时候,也去开辟一块儿空间来存放 sing 方法。开辟了两个空间来存放同一个函数

我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎样做呢?

构造函数原型prototype

构造函数通过原型分配的函数是所有对象所共享的

JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。这个 prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。

我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。

function Star(name, age){
   this.name = name;
   this.age = age;
}
Star.prototype.sing = function() { //直接打点  .protoype 访问到该属性
    console.log('sing');
}
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 19);
ldh.sing();
zxy.sing();// 但是这里是怎样访问到呢?
console.log(ldh.sing === zxy.sing)  // true

大家可以这样理解构造函数里面的内容:

构造函数.png

prototype对象中除了我们自己添加的方法,还有许多内置的属性和方法,也就是说每个构造函数它其实都有许多已经内置的属性和方法放在prototype里面,这个prototype不是我们要用到的时候才创建的,是本来就存在的,就不展开讲了

一般情况下,公共的属性定义到构造函数里面(静态成员),公共的方法放在原型对象身上。

根据上面说的我们可以知道,prototype是构造函数的一个属性,也就说明可以通过 构造函数.prototype 的方式访问到内部的原型,但是为什么我们创造出来的对象也可以访问到原型里面的内容呢?

对象原型 __ proto __

对象都会有一个属性 __ proto __ 指向构造函数的 prototype 原型对象。对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __ proto __ 原型的存在。

  • __ proto __ 对象原型和原型对象 prototype 是等价的
  • __ proto __ 对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype

原型图1.webp

此外,对象原型( __ proto __ )构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。

constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。

因此完全的关系图是这样的:

原型图2.png

那么又引出了一个新的问题,设想我们所说的,我们创建一个具体的对象的时候,需要类这样的概念作为模板,举一个例

我们刚刚定义的明星Star的原型是不是有可能来自于更上一层的构造函数创建生成出来的?更上的一层是不是可能还有更上?

举一个不太恰当的实际性的例子:

原型链.png

当你说高数这个实例的时候,它的基础其实是大学数学(以一定的依据划分下来),而大学数学的基础是数学.....

我们在往一个更加宏观的方向发展,这其中就存在一个关系,原型链

关于为什么指向object原型对象,原型对象本质上还是一个对象,因此它的原型对象还是object的原型对象,往下看

原型链1.png

  1. 只要是对象就有 __ proto __ 原型,指向原型对象
  2. Star 原型对象里面的 __ proto __ 原型 指向的是 Object.prototype
  3. Object.prototype 原型对象里面的 __ proto __ 原型 指向为 null

只要是对象,它里面都有一个原型 __ proto __ ,它指向的是原型对象 prototype,原型对象里面也有一个 __ proto __ ,它指向的是 object 原型对象 prototype,object 原型对象里面也有一个 proto __ , 它指向是 null

简单来说就是,每一个对象都有一个原型,每一个原型又是一个对象,所以原型又有自己的原型,这样一环扣一环形成一条链,就叫原型链。

根据我们所说的,构造函数的prototype和他生成的实例的proto其实是一个东西对吧,也就是说可以认为star原型对象是由object构造函数生成的一个实例,那么就更加说明star的原型是来自于object的。

原型链2.png

JavaScript 的成员查找机制(规则)

  ① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。

  ② 如果没有就查找它的原型(也就是 __ proto__ 指向的 prototype 原型对象)。

  ③ 如果还没有就查找原型对象的原型(Object的原型对象),即一层一层往上找。

  ④ 依此类推一直找到 Object 为止(null)。

  ⑤ __ proto__ 对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

因此我们就可以通过原型链来调用实例本身没有定义的方法或者访问相应的属性。

ES6的内容其实还有很多,我们暂且就重点学习这些知识,这节课就到此结束咯,加油!

作业

提示:各个学长留的作业一般都是实际中常用的技巧或是常考的面试题目哦😇

作业按能力挑选几个完成即可(つ´ω`)つ😇

作业1

用尽可能多的方法实现数组降维。

输入为[1, 2, [3, 4, 5, [6, 7], [8, 9, [10]]], [11, 12]]
输出为[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

作业2

利用rest参数,实现一个累积函数,不管传入多少参数都能够输出正确结果。

作业3

查阅函数柯里化的相关资料,通过自学,实现如下功能的函数:

add(1)(2)(3); // 6 

zh.javascript.info/currying-pa…

作业4

理解基本数据类型和引用数据类型后,查阅相关资料,尝试自己实现一个浅拷贝和深拷贝。(尽力去完成)

作业5

学习防抖节流函数,并尝试自己完成一个:

vue3js.cn/interview/J…