let关键字
声明格式
1 | let a; |
- 变量不能重复声明
1
2let star ='罗志祥';
let star ='小猪'; - 块级作用域 全局 ,函数 , eval
1
2
3
4
5if else while for
{
let girl = '周扬青';
}
console.log(girl); //输出错误
实例演示
1 | var a = []; |
上面代码中,变量i
是var
命令声明的,在全局范围内都有效,所以全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
。也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是 10。
如果使用let
,声明的变量仅在块级作用域内有效,最后输出的是 6。
比较
1 | var a = []; |
上面代码中,变量i
是let
声明的,当前的i
只在本轮循环有效,所以每一次循环的i
其实都是一个新的变量,所以最后输出的是6
。你可能会问,如果每一轮循环的变量i
都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i
时,就在上一轮循环的基础上进行计算。
1 | <body> |
此时点哪个li就会出现当前的index,完美解决了之前的问题
- 不存在变量提升
1 | console.log(song); |
- 不影响作用域链
1
2
3
4
5let school = '尚硅谷'
function fn(){
console.log(school);
}
fn() //可以打印出来结果 - 暂时性死去
const声明常量
一定要赋初始值
1
const A; // SyntaxError
一般常量使用大写(潜规则)
1
const A=100
常量的值不能修改
1
SCHOOL = 'ATGUIGU' //控制台报错
块级作用域
1
2
3
4{
const PLAYER ='UZI';
}
console.log(PLAYER); //控制台报错对于数组和对象的元素修改,不算做对常量的修改,不会报错
1
2
3const TEAM = ['UZI','MLXG','MING','LETME'];
TEAM.push('Meiko');
TEAM=['CLEARLOVE'] // 报错ES6 允许按照一定模式从数组和对象中提取值,对变量进行赋值,称为结构赋值
数组的结构
1
2
3
4
5
6const F4 =['小沈阳','刘能','赵四','宋小宝' ];
let [xiao,liu,zhao,song]=F4;
console.log(xiao); //小沈阳
console.log(liu); //刘能
console.log(zhao); //赵四
console.log(song); //宋小宝对象的解构
1
2
3
4
5
6
7
8
9
10const zhao = {
name:'赵本山',
age:'不详',
xiaopin:function(){
console.log('我可以演小品');
}
};
let { name, age, xiaopin } = zhao;
// console.log(zhao.xiaopin)
xiaopin()
变量的解构赋值
数组的解构赋值
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
1
2
3let a = 1;
let b = 2;
let c = 3;ES6 允许写成下面这样。
1
let [a, b, c] = [1, 2, 3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []如果解构不成功,变量的值就等于
undefined
1
2let [foo] = [];
let [bar, foo] = [1];以上两种情况都属于解构不成功,
foo
的值都会等于undefined
。另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。
1
2
3
4
5
6
7
8let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
ES6引入新的声明字符串的方式[``]
- 声明
1
let str = `我也是一个字符串哦!`
- 内容中可以直接出现换行符
1
2
3
4
5
6let str =`<ul>
<li>沈腾</li>
<li>玛丽</li>
<li>魏翔</li>
<li>艾伦</li>
</ul> `; - 变量拼接使用${}
1
2
3let lovest=`魏翔`;
let out =`${lovest}是我心目中最搞笑的演员!!!`;
console.log(out);
严格模式
介绍严格模式
JavaScript 除了提供正常模式外,还提供了严格模式(strict mode)。ES5 的严格模式是采用具有限制性 JavaScript 变体的一种方式,即在严格的条件下运行 JS 代码。严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。
严格模式对正常的 JavaScript 语义做了一些更改:
- 消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为。
- 消除代码运行的一些不安全之处,保证代码运行的安全。
- 提高编译器效率,增加运行速度。
- 禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:class, enum, export, extends, import, super 不能做变量名。
开启严格模式
为脚本开启严格模式
为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句“use strict”;
(或'use strict'
)
1 | <script> |
为函数开启严格模式
要给某个函数开启严格模式,需要把“use strict”;
(或 'use strict';
) 声明放在函数体所有语句之前。
例如现在有两个函数,但是我们只想给第一个函数加严格模式,可以进行如下操作:
1 | <script> |
严格模式中的变化
变量规定
1.在正常模式中如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用
var
命令声明,然后再使用。
例如现在有一个函数,我们没有给其变量赋值,在没有给定严格模式前:1
2
3
4
5
6
7
8<script>
function f1(){
num = 10;
console.log('num的值是:'+num);
}
f1(); //num的值是10
</script>如果加入严格模式
1
2
3
4
5
6
7
8
9<script>
function f1(){
'use strict';
num = 10;
console.log('num的值是:'+num);
}
f1() // ReferenceError
</script>严格模式下this的指向问题
以前在全局作用域函数中的
this
指向window
对象。 严格模式下全局作用域中函数中的this
是undefined
。定时器的this指向依旧是window
1
2
3
4
5
6
7<script>
function f1(){
'use strict';
console.log('严格模式下普通函数的this:'+this);
}
f1() // undefined以前构造函数时不加new也可以调用,当普通函数,this指向全局对象
严格模式下,如果构造函数不加new调用,this会报错.
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 非严格模式
function Star() {
this.name = 'xl';
console.log(this)
}
Star() //window()
// 严格模式
function Star() {
'use strict'
this.name = 'xl';
}
var s = new Star();
console.log(s.name);
高阶函数
高阶函数对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出
1 | function fn(callback){ |
第二种闭包
1 | function fn(){ |
闭包
闭包的主要作用延伸了变量的作用范围
1 | function fn() { |
上面的代码在打完断点后可以清晰的看出闭包这一问题
实例
1 | <body> |
callback&&callback()
如果存在回调函数就执行!
这是利用了 JS &&符号的一个小技巧
&& 符号在前面为假时就不会执行后面的语句了
所以这个就相当于
1 | if(callback){ |
ES6简化对象写法
ES6允许在大括号里面,直接写入变量和函数,作为对象的属性和方法
1 | let name ='尚硅谷'; |
ES6箭头函数
声明定义方式
1
() => {}
箭头函数的函数体
1
2
3
4
5
6// 当函数体中只有一条js表达式时,可以省略大括号,且js表达式的执行结果就是return的 返回值
const sum1 =(num1,num2)=>num1+num2;
//等同于==
function sum2(num1,num2){
return num1+num2;
}箭头函数中this是静态的,this始终指向函数声明时所在作用域下的this的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function A(){
console.log(this.name)
}
let B = () => {
console.log(this.name);
}
window.name = '尚硅谷';
const school = {
name: 'ATGUIGU'
}
//直接调用
A() //尚硅谷
B() //尚硅谷
//call
A.call(school); //ATGUIGU
B.call(school); //尚硅谷箭头函数面试题
obj对象不产生作用域
1
2
3
4
5
6
7var obj={
age:20,
say:()=>{
alert(this.age)
}
}
obj.say();//undefined不能作为构造实例化对象
1
2
3
4
5
6let A(name,age)=>{
this.name=name;
this.age=age;
}
let me = new A('xiao',123);
console.log(me) //error不能使用argumrnts变量
1
2
3
4let fn =()=>{
console.log(arguments);
}
fn(1,2,3) //error注:arguments
定义:是一个对应于传递给函数的参数的类数组对象。1
2
3
4
5
6
7
8
9
10
11
12function func1(a, b, c) {
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);简写
a. 省略小括号,当形参有且只有一个的时候1
2
3let add = n =>{
return n + 1;
}b. 省略花括号,当代码体只有一条语句的时候,此时return也必须省略
1
let add = n => n + 1;
函数参数默认值
- 介绍
ES6允许给函数参数赋值初始值 - 特性
- 可以给形参赋初始值,一般位置要靠后(潜规则)
1
2
3
4
5function add(a,b.c=20){
return a+b+c;
}
let result=add(1,2);
console.log(result) //15 - 与解构赋值结合
1
2
3
4
5
6
7
8function A({host='127.0.0.1',username,password,port}){
console.log(host+username+password+port)
}
A({
username:'ran',
password:'123456',
port:3306
})
- 可以给形参赋初始值,一般位置要靠后(潜规则)
rest 参数
ES6引入rest参数,用于获取函数的实参,用来代替arguments
特性
…args为rest参数(放在函数声明形参的位置)
1 | function date(...args){ |
只能写成(a,b,c,…args)
扩展运算符
- 介绍
扩展运算符是能将数组转换为逗号分隔的参数序列 - 特性
1
2
3
4
5
6
7
8
9
10
11
12//chunwan(...tfboys)
const tfboys=['AA','BB','CC']
function chunwan(){
console.log(arguments);
}
chunwan(...tfboys); //0:'AA' 1:'BB' 2:'CC'
//chunwan(tfboys)
const tfboys=['AA','BB','CC']
function chunwan(){
console.log(arguments);
}
chunwan(tfboys); //['AA','BB','CC']是一个数组 - 应用
- 数组的合并
1
2
3
4const A = ['aa','bb'];
const B = ['cc','dd'];
const C = [...A,...B];
console.log(C) //[aa,bb,cc,dd] - 数组的克隆
1
2
3const A = ['a','b','c'];
const B = [...A];
console.log(B) //[a,b,c] - 将伪数组转换为真正的数组
1
2
3
4
5
6<div></div>
<div></div>
<div></div>
const A = documents.querySelectorAll('div');
const B = [...A];
console.log(B) // [div,div,div] - 字符串扩展运算符可以将字符串转化为真正的数组
1
2[...'hello']
// ['h','e','l','l','o']
- 数组的合并
Symbol
- 介绍
ES6引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,是一种类似于字符串的数据类型。
Symbol特点:
Symbol的值是唯一的,用来解决命名冲突的问题
Symbol值不能与其他数据进行运算
Symbol定义的对象属性不能使用for…in循环遍历,但是可以使用Reflect.ownKeys来获取对象的所有键名
特性
1 | let s = Symbol('aa'); |
不能与其他数据进行运算
1 | let result = s + 100 //error |
- 应用
- 给对象添加方法方式一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14let game = {
name : 'ran'
}
let methods = {
up:Symbol(),
down:Symbol()
}
game[methods.up]=function(){
console.log('aaa');
}
game[methods.down]=function(){
console.log('bbb');
}
console.log(game) // name: 'ran',Symbol(),Symbol() - 给对象添加方法方式二
1
2
3
4
5
6
7let youxi = {
name: '狼人杀',
[Symbol('say')]:function(){ //Symbol为动态值
console.log('阿萨德')
}
}
console.log(youxi) // name:'狼人杀',Symbol(say)
instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
1 | // 定义构造函数 |
for in和for of的区别
- 循环数组
区别一:for in和for of都可以循环数组,for in输出的是数组的index下标,而for of输出每一项的值1
2
3
4
5
6
7
8
9
10
11const arr[1,2,3,4]
//for ... in
for (const key in arr){
console.log(key) // 输出 0,1,2,3
}
// for ... of
for (const key of arr){
console.log(key) // 输出 1,2,3,4 - 循环对象
区别二:for in 可以遍历对象,for of 不能遍历对象,只能遍历带有iterator接口的,例如Set,Map,String,Array1
2
3
4
5
6
7
8
9
10
11
12
13
const object = { name: 'lx', age: 23 }
// for ... in
for (const key in object) {
console.log(key) // 输出 name,age
console.log(object[key]) // 输出 lx,23
}
// for ... of
for (const key of object) {
console.log(key) // 报错 Uncaught TypeError: object is not iterable
}
迭代器
- 介绍
- 迭代器(lterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署lterator接口,就可以完成遍历操作。
- 原理:创建一个指针对象,指向数据结构的起始位置,第一次调用==next()==方法,指针自动指向数据结构第一个成员,接下来不断调用next(),指针一直往后移动,直到指向最后一个成员,没调用next()返回一个包含value和done属性的对象
- 特性
1
2
3
4
5
6
7const xiyou=['AA','BB','CC','DD'];
// for(let v of xiyou){
//console.log(v) // 'AA','BB','CC','DD' //for in保存的是键名,for of保存的是键值
// }
let iterator = xiyou[Symbol.iterator]();
console.log(iterator.next()); //{{value:'唐僧',done:false}}
console.log(iterator.next()); //{{value:'孙悟空',done:false}} - 应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28const banji = {
name : "终极一班",
stus: [
'aa',
'bb',
'cc',
'dd'
],
[Symbol.iterator](){
let index = 0;
let _this = this;
return {
next: () => {
if(index < this.stus.length){
const result = {value: _this.stus[index],done: false};
//下标自增
index++;
//返回结果
return result;
}else {
return {value: underfined,done:true};
}
}
}
}
}
for(let v of banji){
console.log(v); // aa bb cc dd
生成器
- 介绍
生成器函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同,是一种特殊的函数 - 特性
1
2
3
4
5
6
7
8
9
10function * gen (){ //函数名和function中间有一个 *
yield '耳朵'; //yield是函数代码的分隔符
yield '尾巴';
yield '真奇怪';
}
let iterator = gen();
console.log(iteretor.next());
//{value:'耳朵',done:false} next()执行第一段,并且返回yield后面的值
console.log(iteretor.next()); //{value:'尾巴',done:false}
console.log(iteretor.next()); //{value:'真奇怪',done:false} - 应用
生成器函数的参数传递用生成器函数的方式解决回调地狱问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function * gen(args){
console.log(args);
let one = yield 111;
console.log(one);
let two = yield 222;
console.log(two);
let three = yield 333;
console.log(three);
}
let iterator = gen('AAA');
console.log(iterator.next());
console.log(iterator.next('BBB')); //next中传入的BBB将作为yield 111的返回结果
console.log(iterator.next('CCC')); //next中传入的CCC将作为yield 222的返回结果
console.log(iterator.next('DDD')); //next中传入的DDD将作为yield 333的返回结果模拟异步获取数据1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27function one(){
setTimeout(()=>{
console.log('111')
iterator.next()
},1000)
}
function two(){
setTimeout(()=>{
console.log('222')
iterator.next();
},2000)
}
function three(){
setTimeout(()=>{
console.log('333')
iterator.next();
},3000)
}
function * gen(){
yield one();
yield two();
yield three();
}
let iterator = gen();
iterator.next();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31function one(){
setTimeout(()=>{
let data='用户数据';
iterator.next(data)
},1000)
}
function two(){
setTimeout(()=>{
let data='订单数据';
iterator.next(data)
},2000)
}
function three(){
setTimeout(()=>{
let data='商品数据';
iterator.next(data)
},3000)
}
function * gen(){
let users=yield one();
console.log(users)
let orders=yield two();
console.log(orders)
let goods=yield three();
console.log(goods)
}
let iterator = gen();
iterator.next();
Promise
- 介绍
Promise是ES6引入的异步编程的新解决方案。语法上 Promise是一个构造函数,用来封装异步操作并可以获取其成功或失败的结果。 - 特性
基本特性Promise.then()方法1
2
3
4
5
6
7
8
9
10
11
12
13const p = new Promise((resolve,reject)=>{
setTimeout(()=>{
let data='数据库数据'
//resolve(data); //正确时调用
reject(data); //错误时调用
})
})
p.then(function(value){
console.log(value)
},function(reason){
console.log(reason)
}
)发送ajax请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const p =new Promise((resolve, reject) =>{
setTimeout(()=>{
resolve('用户数据');
})
});
//then()函数返回的实际也是一个Promise对象
//1.当回调后,返回的是非Promise类型的属性时,状态为fulfilled,then()函数的返回值为对象的成功值,如reutnr 123,返回的Promise对象值为123,如果没有返回值,是undefined
//2.当回调后,返回的是Promise类型的对象时,then()函数的返回值为这个Promise对象的状态值
//3.当回调后,如果抛出的异常,则then()函数的返回值状态也是rejected
let result = p.then(value => {
console.log(value)
// return 123;
// return new Promise((resolve, reject) => {
// resolve('ok')
// })
throw 123
},reason => {
console.log(reason)
})
console.log(result);使用p.then实现链式回调1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38<script>
//ajax请求返回一个promise
function sendAjax(url){
return new Promise((resolve, reject) => {
//创建对象
const x =new XMLHttpRequest();
//初始化
x.open('GET',url);
//发送
x.send();
//时间绑定
x.onreadystatechange = ()=>{
if(x.readyState === 4 ){
if(x.status >= 200 && x.status < 300){
//成功
resolve(x.response)
}else {
//失败
reject(x.status)
}
}
}
})
}
//测试
sendAjax("https://api.apiopen.top/getJoke").then(value => {
console.log(value)
},reason => {
console.log(reason)
})
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const p=new Promise((resolve,reject)=>{
fs.readFile("./resources/为学.md",(err,data)=>{
resolve(data);
})
})
//利用p.then
p.then(value =>{
return new Promise((resolve,reject)=>{
fs.readFile("./resources/插秧诗.md",(err,data)=>{
resolve([value,data]);
})
})
}).then(value =>{
return new Promise((resolve,reject)=>{
fs.readFile("./resources/观书有感.md",(err,data)=>{
//压入
value.push(data);
resolve(value);
})
})
})
Promise.catch()方法
作为一个语法糖,与then的使用方式类似,只有一个参数
1 | //catch()函数只有一个回调函数,意味着如果Promise对象状态为失败就会调用catch()方法并且调用回调函数 |
set
- 介绍
ES6提供了新的数据结构set(集合)。它类似于数组,但成员的值都是唯一的,集合实现了iterator接口,所以可以使用「扩展运算符』和「 for…of…』进行遍历,集合的属性和方法:- size返回集合的元素个数
- add增加一个新元素,返回当前
- delete删除元素,返回bollean值has检测集合中是否包含某个元素,返回boolean值
- 特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let s = new Set();
let s2 = new Set(['A','B','C','D'])
//元素个数
console.log(s2.size);
//添加新的元素
s2.add('E');
//删除元素
s2.delete('A');
//检测
console.log(s2.has('C'));
//清空
s2.clear()
console.log(s2); - 应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let arr = [1,2,3,4,5,4,3,2,1]
//1.数组去重
let result = [...new Set(arr)] //new Set(arr)可将其转化为类数组,而且里面元素不重复,扩展运算符...是将类数组转化为数组
console.log(result);
//2.交集
let arr2=[4,5,6,5,6]
let result2 = [...new Set(arr)].filter(item => new Set(arr2).has(item))
console.log(result2);
//3.并集
let result3=[...new Set([...arr,...arr2])]
console.log(result3);
//4.差集
let result4= [...new Set(arr)].filter(item => !(new Set(arr2).has(item)))
console.log(result4);
Map
- 定义
ES6提供了Map数据结构。它类似于对象,也是键值对的集合。但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
注:Map也实现了iterator接口,所以可以使用『扩展运算符』和「for…of…』进行遍历。Map的属性和方法。 - 特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27let m = new Map();
m.set('name','ran');
m.set('change',()=>{
console.log('改变!')
})
let key={
school:'atguigu'
}
m.set(key,['成都','西安']);
//size
console.log(m.size);
//删除
m.delete('name');
//获取
console.log(m.get('change'));
// //清空
// m.clear()
//遍历
for(let v of m){
console.log(v);
}
Class
- 介绍
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。 - 特性静态成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14class shouji { //与构造函数类似
constructor(brand,price) { //下面创建实例时执行了constructor函数
this.brand=brand;
this.price=price
}
call(){
console.log('我可以打电话')
}
}
let A = new shouji('1+',1999); //实例化对象
console.log(A)1
2
3
4
5
6
7class Person{
static name='手机';
price=5999
}
let nokia = new Person();
console.log(nokia.name); //undefined
console.log(nokia.price) //5999
构造函数的继承
1 |
|
复习构造函数及原型(prototype)
超级好的一篇博客2020面试收获 - js原型及原型链 - 掘金 (juejin.cn)
引进博客(49条消息) JS中的原型和原型链(图解)_d_ph的博客-CSDN博客_js原型和原型链
构造函数
- 创建一个构造函数,专门用来创建一个指定对象的
- 构造函数就是普通函数,创建方式和普通函数没有区别,不同的是构造函数习惯上首字母大写
- 构造函数和普通函数的区别就是调用方式不同(普通函数是直接调用的,而构造函数需要使用new关键字来调用)
- 构造函数的执行流程
- 立刻创建一个对象
- 将新建的对象设置为函数中this,在构造函数中可以使用this来引用新建的对象
- 遂行执行函数中的代码
- 将新建的对象作为返回值返回
- 使用同一个构造函数创建的对象,我们称为一类对象,也将一个构造函数称为一个类。我们将通过一个构造函数创建的对象,称为是该类的实例
- this的情况
- 当以函数的形式调用时,this是window
- 当以方法的形式调用时,谁调用方法this就是谁
- 当以构造函数的形式调用时,this就是新创建的那个对象
1 |
|
构造函数分为 实例成员 和 静态成员
实例成员: 实例成员就是在构造函数内部,通过this添加的成员。实例成员只能通过实例化的对象来访问。
静态成员: 在构造函数本身上添加的成员,只能通过构造函数来访问
1 | function Star(name,age) { |
通过构造函数创建对象该过程也称作实例化
new一个新对象的过程,发生了什么?
1 | function Father(name) { |
(1) 创建一个空对象 son {}
(2) 为 son 准备原型链连接 son.__proto__ = Father.prototype
(3) 重新绑定this,使构造函数的this指向新对象 Father.call(this)
(4) 为新对象属性赋值 son.name
(5) 返回this return this
,此时的新对象就拥有了构造函数的方法和属性了
每个实例方法并非共享
方法一:在构造函数上直接定义方法(不共享)
1 | function Star() { |
很明显,stu1 和 stu2 指向的不是一个地方。 所以 在构造函数上通过this来添加方法的方式来生成实例,每次生成实例,都是新开辟一个内存空间存方法。这样会导致内存的极大浪费,从而影响性能。
方法二:通过原型添加方法(共享)
构造函数通过原型分配的函数,是所有对象共享的。
1 | function Star(name) { |
原型
注意以下几点
1、所有的引用类型(数组、函数、对象)可以自由扩展属性(除null以外)。
2、所有的引用类型都有一个’_ _ proto_ _’属性(也叫隐式原型,它是一个普通的对象)。
3、所有的函数都有一个’prototype’属性(这也叫显式原型,它也是一个普通的对象)。
4、所有引用类型,它的’_ _ proto_ _’属性指向它的构造函数的’prototype’属性。
5、当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它的’_ _ proto_ _’属性(也就是它的构造函数的’prototype’属性)中去寻找。
定义
Father.prototype 就是原型,它是一个对象,我们也称它为原型对象。
作用
原型的作用,就是共享方法。
我们通过 Father.prototype.method
可以共享方法,不会反应开辟空间存储方法。
proto
[正确解释___proto___](用自己的方式(图)理解constructor、prototype、__proto__和原型链 - 掘金 (juejin.cn))
- 实例对象.proto = 创建自己的构造函数内部的prototype(原型对象)
- 实例对象.proto.constructor = 创建自己的构造函数
所有函数的__proto__指向他们的原型对象,即Function函数的prototype对象
所有的函数都是Function函数的实例(包括Function自己),所以他们的__proto__自然也就都指向Function函数的prototype对象。
最后一个prototype对象是Object函数内的prototype对象。
Object函数作为JS的内置对象,也是充当了很重要的角色。Object函数是所有对象通过原型链追溯到最根的构造函数。换句话说,就是官方动作,不讲道理的神仙操作
Object函数的prototype中的__proto__指向null。
这是由于Object函数的特殊性,有人会想,为什么Object函数不能像Function函数一样让__proto__属性指向自己的prototype?答案就是如果指向自己的prototype,那当找不到某一属性时沿着原型链寻找的时候就会进入死循环,所以必须指向null,这个null其实就是个跳出条件
原型链
什么是原型链
原型与原型层层相链接的过程即为原型链。
定义
对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在
每个对象都有__proto__原型的存在
1 | // 构造函数 |
首先,fn的构造函数是Foo()。所以:fn._ _ proto _ _=== Foo.prototype
又因为Foo.prototype
是一个普通的对象,它的构造函数是Object
,所以:Foo.prototype._ _ proto _ _=== Object.prototype
通过上面的代码,我们知道这个toString()
方法是在Object.prototype
里面的,当调用这个对象的本身并不存在的方法时,它会一层一层地往上去找,一直到null
为止。
所以当fn调用toString()时,JS发现fn中没有这个方法,于是它就去Foo.prototype中去找,发现还是没有这个方法,然后就去Object.prototype中去找,找到了,就调用Object.prototype中的toString()方法。
另外,在使用原型的时候,一般推荐将需要扩展的方法写在构造函数的prototype属性中,避免写在_ _ proto _ _属性里面。
原型链引入的新的继承方式
1 | function GrandFather() { |
Father函数和Son函数都丢弃了它们各自的prototype对象,指向一个新的对象。这形成了三个新的有趣现象:
- Father函数中的prototype指向了GrandFather的实例对象,这时候这个实例对象就成为了Father函数以后实例的原型对象,顺其自然GrandFather实例对象内的私有属性name就变成了Father函数以后实例的共享属性;
- 同样的,Son函数中的prototype指向了Father的实例对象,将Father的实例对象内的私有属性age就变成了Son函数以后实例的共享属性。
- 它们的__proto__属性将它们串了起来,形成一条新的原型链。
class(类)基本语法
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
简述类和对象的区别
类抽象了对象的公共部分,它泛指某一大类(class)
对象特指某一个,通过类实例化一个具体的对象
基本上,ES6 的class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class
改写,就是下面这样。
1 | class Point { |
上面代码定义了一个“类”,可以看到里面有一个constructor()
方法,这就是构造方法,而this
关键字则代表实例对象。
类的构造函数
每一个类都可以有一个自己的构造函数,这个名称是固定的constructor
,当我们通过new
调用一个类时,这个类就会调用自己的constructor
方法(构造函数)。
- 它用于创建对象时给类传递一些参数
- 每一个类只能有一个构造函数,否则报错
通过new
调用一个类时,会调用构造函数,执行如下操作过程:
- 在内存中开辟一块新的空间用于创建新的对象
- 这个对象内部的
__proto__
属性会被赋值为该类的prototype
属性 - 构造函数内的this,指向创建出来的新对象
- 执行构造函数的内部代码
- 如果函数没有返回对象,则返回
this
1 | class Animal { |
上面这个例子中,我们在class
中定义的constructor
,这个就是构造方法,而this
代表的是实例对象。
这个class
,你可以把它看作构造函数的另外一种写法,因为它和它的构造函数的相等的,即是类本身指向构造函数。
1 | console.log(Animal === Animal.prototype.constructor); // true |
其实,在类上的所有方法都会放在prototype
属性上。
类中的共有的属性和方法一定要加this使用
上代码
1 | <body> |
constructor()方法
constructor()
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor()
方法,如果没有显式定义,一个空的constructor()
方法会被默认添加。
实例对象本身没有该属性
1.藏在自身的原型对象中
2.*prototype对象除外
1 | class Point { |
上面代码中,定义了一个空的类Point
,JavaScript 引擎会自动为它添加一个空的constructor()
方法。
深度理解constuctor()方法
1 | function Person() {} |
person1与person2是Person对象的实例,他们的constructor指向创建它们的构造函数,即Person函数;
Person是函数,但同时也是Function实例对象,它的constructor指向创建它的构造函数,即Function函数;
至于Function函数,它是JS的内置对象,在第一点我们就已经知道它的构造函数是它自身,所以内部constructor属性指向自己。
所以constructor属性其实就是一个拿来保存自己构造函数引用的属性,没有其他特殊的地方。
类中的属性
实例属性
实例的属性必须定义在类的方法里
1 | class Animal{ |
静态属性
当我们把一个属性赋值给类本身,而不是赋值给它prototype
,这样子的属性被称之为静态属性(static
)。
静态属性直接通过类来访问,无需在实例中访问。
1 | class Foo{ |
私有属性
私有属性只能在类中读取、写入,不能通过外部引用私有字段。
1 | class Animal{ |
类中的方法
在类上的所有方法都会放在prototype
属性上。
实例方法
在ES6
之前,我们定义类中的方法是类中的原型上进行定义的,防止类中的方法重复在多个对象上
1 | function Animal() {} |
在ES6
中,定义类中的方法更加简洁,直接在类中定义即可,这样子的写法即优雅可读性也强。
1 | class Animal{ |
静态方法
1 | class Animal{ |
Class的类继承
extends
关键字用于扩展子类,创建一个类作为另外一个类的一个子类。
它会将父类中的属性和方法一起继承到子类的,减少子类中重复的业务代码。
这对比之前在ES5
中修改原型链实现继承的方法的可读性要强很多,而且写法很简洁。
extends的使用
1 | class Point{ |
上面示例中,Point
是父类,ColorPoint
是子类,它通过extends
关键字,继承了Point
类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point
类。
继承类的属性和方法
Super
super关键字用于访问和调用一个对象的父对象上的函数。
super
指的是超级、顶级、父类的意思
在子类的构造函数中使用this
或者返回默认对象之前,必须先通过super
调用父类的构造函数。
子类在构造函数中使用super,必须放到this前面(必须先调用父类的构造方法,在使用子类的构造方法)
1 | class A {} |
就近原则
1 | class Father{ |
如果使用super关键字
1 | class Father{ |
Getter和Setter
访问器属性
它们本质上是用于获取和设置值的函数,但从外部代码来看就像常规属性。
访问器属性由”getter”和”setter”方法表示。在对象字面量中,他们用get
和set
表示
1 | let obj = { |
当读取 obj.propName
时,getter 起作用,当 obj.propName
被赋值时,setter 起作用。
理解与运用
例如,我们有一个具有 name
和 surname
属性的对象 user
,现在我们想添加一个 fullName
属性,该属性值应该为 "John Smith"
。当然,我们不想复制粘贴已有的信息,因此我们可以使用访问器来实现:
1 | let user = { |
从外表看,访问器属性看起来就像一个普通属性。这就是访问器属性的设计思想。我们不以函数的方式 调用 user.fullName
,我们正常 读取 它:getter 在幕后运行。
截至目前,fullName
只有一个 getter。如果我们尝试赋值操作 user.fullName=
,将会出现错误:
1 | let user = { |
让我们通过为 user.fullName
添加一个 setter 来修复它:
1 | let user = { |
现在,我们就有一个“虚拟”属性。它是可读且可写的。
在类内部也可以使用get
和set
关键字,对应某个属性设置存值和取值函数,拦截属性的存取行为。
1 | class Animal { |
类的prototype属性和__ proto __属性
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__
属性,指向对应的构造函数的prototype
属性。Class 作为构造函数的语法糖,同时有prototype
属性和__proto__
属性,因此同时存在两条继承链。
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
1 | class A { |
上面代码中,子类B
的__proto__
属性指向父类A
,子类B
的prototype
属性的__proto__
属性指向父类A
的prototype
属性。
数值扩展
1 | <script> |
对象方法的扩展
1 | <script> |
暴露语法汇总
export命令用于规定模块的对外接口
inport命令用于输入其他模块提供的功能
- 统一暴露
1
2
3
4
5
6//统一暴露
let school = '尚硅谷';
function findjob(){
console.log('找工作吧');
}
//export {school,findjob} - 默认暴露
1
2
3
4
5
6
7//默认暴露
export default {
school:'ATGUIGU',
change:function(){
console.log('我们可以改变你')
}
}
引入语法汇总
- 通用导入方式
1
2
3import * as m1 from "./src/js/m1.js"
import * as m2 from "./src/js/m2.js"
import * as m3 from "./src/js/m3.js" - 解构赋值方式
1
2
3
4import {school,teach} from "./src/js/m1.js"
import {school as guigu,findJob} from "./src/js/m2.js"
import {default as m3 } from "./src/js/m3.js" - 简便形式(只针对默认暴露)
1
import m3 from "./src/js/m3.js"
async函数
async和await两种语法结合可以让异步代码像同步代码一样
async函数:
- async函数的返回值为promise对象
- async返回的promise对象的结果值由async函数执行的返回值决定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15async function fn(){
//1.如果返回的是一个非Promise的对象,则fn()返回的结果就是成功状态的Promise对象,值为返回值
//2.如果返回的是一个Promise对象,则fn()返回的结果与内部Promise对象的结果一致
//3.如果返回的是抛出错误,则fn()返回的就是失败状态的Promise对象
return new Promise((resolve,reject)=>{
resolve('成功的数据');
});
}
const result = fn();
result.then(value=>{
console.log(value) //成功的数据
},reason=>{
console.log(reason)
})
await表达式
- await必须放在async函数中
- await右侧的表达式一般为promise对象
- await可以返回的是右侧promise成功的值
- await右侧的promise如果失败了,就会抛出异常,需要通过try…catch捕获处理
发送AJAX请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38<script>
//ajax请求返回一个promise
function sendAjax(url){
return new Promise((resolve, reject) => {
//创建对象
const x =new XMLHttpRequest();
//初始化
x.open('GET',url);
//发送
x.send();
//时间绑定
x.onreadystatechange = ()=>{
if(x.readyState === 4 ){
if(x.status >= 200 && x.status < 300){
//成功
resolve(x.response)
}else {
//失败
reject(x.status)
}
}
}
})
}
//async 与 await 测试
async function main(){
let result = await sendAjax("https://api.apiopen.top/getJoke")
console.log(result);
}
main()
</script>
this指向
this作用域问题
先看代码
非常重要
1 | function foo() { |
上段代码中我们在 foo
函数内部使用 this
调用了 bar
函数,然后在 bar
函数内部打印 a
变量,如果我们按照作用域链的思想思考的话,此时的 a
变量按道理是能够读取到的,但是事实却是 undefined
。
造成上述问题的原因有多个,其中有一个就是 this 在任何情况下都不指向函数的词法作用域,上段代码就使用使用 this
将 foo
和 bar
函数的词法作用域联通,这是不可行的。
1 | var obj = { |
this的定义
this
就是一个对象,this
是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
四种方式(绑定规则)
默认绑定
我们比较常见的一种函数调用类型就是独立函数的调用,形如foo()
等。这个时候的 this
绑定就是采用的默认绑定规则。
当函数不带用任何修饰进行调用时,此时 this
的绑定就是默认绑定规则,this
指向全局对象。
箭头函数没有this
1 | var name = '小猪课堂'; |
上段代码非常简单,我们在全局作用域中定义了一个变量name
,然后我们在函数 foo
中使用this.name
,输出的结果就是全局变量name
,这说明我们 this
指向了全局作用域,也就是说 this
绑定到了 window
对象上。
隐式绑定
前面的默认绑定规则很好理解,因为我们的函数执行上下文就是全局作用域,this
自然而然绑定到了全局对象上。
独立函数的调用我们可以直接看出执行上下文在哪里,但如果不是独立函数调用,比如下面代码。
1 | function foo() { |
上段代码我们在 obj
对象中引用了函数 foo
,然后我们使用 obj.foo
(函数别名)的方式调用了该函数,此时不是独立函数调用,我们不能使用默认绑定规则。
此时 this
的绑定规则称为隐式绑定规则,因为我们不能直接看出函数的调用位置,它的实际调用位置在 obj
对象里面,调用 foo
时,它的执行上下文对象为 obj
对象,所以 this
将会被绑定到 obj
对象上,所以我们函数中的 this.name
其实就是obj.name
。这就是我们的隐式绑定规则。
显式绑定
前面我们已经说了默认绑定和隐式绑定,其中隐式绑定我们通常是以 obj.foo
这种形式来调用函数的,目的就是为了让 foo
的 this
绑定到 obj
对象上。
这个时候,如果我们不想通过 obj.foo
的形式调用函数,我们想要很明确的将函数的 this
绑定在某个对象上。那么可以使用 call
、apply
等方法,这就是所谓的显式绑定规则。
1 | function foo() { |
new绑定
new
关键词相信大家都知道或者使用过吧,这就是我们将要将的第 4
种 this
绑定,叫做 new
绑定。
想要知道 new
绑定规则,我们就很有必要知道一个当我们 new
一个对象的时候做了什么,或者说 new
关键词会做哪些操作。
使用 new 来调用函数时,会执行下面操作:
- 创建一个全新的对象
- 这个新对象会被执行原型连接
- 这个新对象会绑定到函数调用的
this
- 如果函数没有返回其它对象,那么
new
表达式种的函数调用会自动返回这个新对象
我们可以看到 new
的操作中就有 this
的绑定,我们在来看看代码。
1 | function foo(name) { |
this 绑定规则优先级:
默认绑定 < 隐式绑定 < 显式绑定 < new 绑定
面试官:JS中this指向哪儿?你是如何确定this的? - 掘金 (juejin.cn)
改变函数内的this指向 js提供了三种方法call() apply()bind()
call方法
call()可以调用函数
-call()可以改变这个函数的this的指向call()可以改变这个函数的this的指向
call的主要作用可以实现继承
1
2
3
4
5
6
7
8
9function fn(x,y){
console.log('我想喝手磨咖啡')
console.log(this)
console.log(x+y)
}
var o={
name:'andy'
}
fn.call(o,1,2) // o代表this指向 1,2是参数
apply方法
- 也是调用函数 第二个可以改变函数内部的this指向
- 但是它的参数必须是数组(伪数组)
1 | var o={ |
apply的主要应用
1
2
3
4
5
6
7
8function fn(arr){
consloe.log(this);
consloe.log(arr);
};
fn.apply(o,['pink'])
var arr=[1,66,3,99,4];
var max=Math.max.apply(Math,arr);
console.log(max)
bind方法
不会调用原来的函数 可改变函数内部的this指向
返回的是原函数改变this之后产生的新函数
1
2
3
4
5
6
7
8
9var o = {
name: 'andy'
};
function fn() {
console.log(this)
}
// fn.bind(o) // 控制台不会打印对象,因为bind函数会生成一个新函数
var f = fn.bind(o);
f(); // 可以看出bind方法不会调用函数如果有的函数我部门不需要调用,但是又想改变这个函数内部的this的指向此时用bind
我们有一个按钮,当我们点击之后就禁用这个按钮,3秒钟之后开启这个按钮
1
2
3
4
5
6
7
8
9
10
11
12var btn =document.querSelector('button');
btn.onclick=function(){
this.disabled=true; // 这个this指向的是btn这个按钮
// var that=this;
setTimeout(function(){
// that.disabled=false
// 定时器函数里面的this指向是window
this.disabled=false;
// 定时器函数里面的this指向的是window
}.bind(this),3000)
// bind这个函数在定时器外面
}
立即执行函数—-IIFE
立即执行函数是指指函数声明后立即执行,也就是说函数自己调用自己
语法:
直接在函数声明后加括号
1 | function() { |
变量函数
1 | var demo = function(){ |
变量名不加括号时,这个变量名就代 表整个函数,加括号时代表函数的调用
1 | var fun1=function(){ |
函数返回值
函数返回值—–return
- 返回值的类型由return后面的值决定的,返回值可以是任意数据类型
- return后没有值,会返回undefined
- 函数不写返回值也会返回undefined
- 函数中return就代表函数执行完毕,后面的语句不会再执行
1 | function a(){ |