变量声明,作用域
众所周知,JavaScript变量声明共有三种关键字 var let const
对于var,在 ECMAScript 的所有版本中均可使用,而let与const只能在ECMAScript6(ES6)及之后的版本中使用。然而ES6新增的let与const直接受到了大多数程序猿的追捧即大多数童鞋不再使用var而只使用const,下面就让小王和大家好好掰扯一下这些关键字(说的不好或者不对的地方望大家能够指正) 要搞明白这三种关键字首先我们需要了解作用域
作用域(Scope)
作用域就是变量与函数的可访问范围,作用域决定了代码区块中变量与其他资源的可访问性(可见性),在ES6之前,只存在全局作用域和局部作用域。ES6之后,出现了块级作用域。
全局作用域
全局作用域顾名思义就是作用域是全体即所有脚本与函数均能访问到,函数之外声明的变量就是全局变量
var name = "WDY";
function my(){
//里面可以调用name
}
局部作用域
局部作用域顾名思义就是作用域是局部即不是所有均能访问,函数之内的声明就是局部作用域
function my(){
var name = "WDY";
}
//不可访问name
块级作用域(ES6新增)
由最近一对包含花括号界定{} 即if块,while块,function块,甚至是单独的块
if(true){
let name = "WDY";
}
//外部不可访问
while(true){
let name = "WDY";
}
//外部不可访问
var
1. 声明范围(函数作用域)
function sayHi(){
var m = "Hi";
}
sayHi();
console.log(m) //报错,不可访问
//这也表示局部变量在函数退出时就被销毁了
2.变量提升(绝活儿)
使用var关键字声明的变量会自动提升到函数作用域顶部
function sayName(){
console.log(name);
var name = "WDY";
}
sayName(); //undefined
//!!注意变量提升意味着不会报错而是输出undefined,虽然提升了,但是不会读到值,所以不会输出WDY
//即可以后定义变量,在函数中它相当于在顶部命名即
function sayName(){
var name = "WDY";
console.log(name);
}
function sayName(){
console.log(name);
let name = "WDY"; //报错
}
let
1.声明范围(块级作用域)
if(true){
var name = "WDY";
console.log(name); //WDY
}
console.log(name); //WDY
if(true){
let name = "WDY";
console.log(name); //WDY
}
console.log(name); //报错:name没有定义
let不允许同一块级作用域中重复声明
var name;
var name; //可行
let name;
let name; //报错
不在同一块级作用域中可以重复声明
let name = "WDY";
console.log(name); //WDY
if(true){
let name = "王大宇";
console.log(name); //王大宇
}
2.全局声明
在let全局作用域中声明的变量不会成为window对象的属性(var会吗?会!)
var name = "WDY";
console.log(window.name); //WDY
let name = "WDY";
console,log(window.name); //undefined
const
1.声明范围(块级作用域)
2.不允许重复声明
3.声明变量时必须初始化变量,且变量一经声明不可修改,声明对象后对象不可修改但其键可以
const name; //×
const name = "WDY";
name = "傻子"; //×
const P1 = {};
p1 = {} //×
const p1 = {};
p1.name = "WDY";
console.log(p1.name);
//如果想让整个对象都不能修改可以使用Object.freeze(),不会报错,但不会成功
const p1 = Object.freeze({});
p1.name = "WDY";
console,log(p1.name); //undefined
//在循环中迭代变量时使用三者
for(var i = 0; i < 5; i++){
console.log(i); //1,2,3,4,5
}
console.log(i); //5
for(let i = 0; i <5; i++){
console.log(i); //1,2,3,4,5
}
console.log(i); //未定义
for(const i = 0; i < 5; i++){
console.log(i); //5,5,5,5,5
}
for(const i of [1,2,3,4,5]){
console.log(i); //1,2,3,4,5
}
变量声明,作用域 -----> ending!
数据类型
JavaScript共有8中数据类型,其中,7种为简单数据类型(原始类型),他们是:Undefined,Null,Number,String,Boolean,Symbol(符号<为ES6新增类型>),BigInt。还有一种复杂数据类型(引用类型):Object。
其中6中简单数据类型属于原始值1种复杂数据类型属于引用值。在讨论数据类型之前,小王现在这里说一下这两种不同类型的数据。
原始值与引用值
保存原始值的变量是按值访问的,而引用值是保存在内存中的对象。 当我们操作原始值时,我们操作的就是存储在变量中的实际值,而当我们操作引用值时,JavaScript不允许直接访问内存的位置,因此我们不能直接操作对象本身所在的内存空间而是操作对该对象的引用。 我认为,这两种类型的区别在变量复制方面的得到了充分地体现:
对于原始值,复制之后与复制之前是完全独立的,而对于引用值,复制后的值与复制之前的值指向同一内存空间,所以两者相当于铁索连环啦哈哈哈。
接下来让我们进入今天的正题:
Undefined
undefined即未定义的,其效果如字面意思,当我们使用var/let声明一个变量但未给其赋值时,该变量即为undefined(!注意const可不兴这的搞啊,使用const的前提就是声明的同时必须给其赋值),当然我们也可以给一个变量赋值为undefined但其效果等同于只声明而不赋值:
let test;
console.log(test); //undefined
let test = undefined;
console.log(test); //undefined
对于为声明的变量,其返回也是undefined
Null
null即空的,其值表示一个空指针对象,如果我们声明一个变量但未确定给其赋何值时,我们可以将null赋给他,这样当后面我们需要知道这个是否被声明时,只要将其与null比较或者看他数据类型即可,undefined不行吗?undefined不行,因为为声明的值其返回值也可能是undefined。
Boolean
布尔值,只有两个值true和false对于这两个值,有两点需特别注意:
- 两个布尔值不等同于数值即true不等于1,false不等于0。
- 布尔值区分大小写,虽然True与False也有效,但不能将其认为是布尔值。
布尔值可以通过Boolean()将布尔类型与其他数据类型联系起来:
| 数据类型 | true | false |
|---|---|---|
| String | 非空字符串 | "" |
| Number | 非0值 | 0/NaN |
| Object | 任意对象 | Null |
| Undefined | undefined | |
| Null |
Number
不同于其他语言,JavaScript的整数和浮点数均为Number,但其存储时使用的内存空间大小不同,存储浮点值使用的内存空间是存储整数值得两倍,所以你懂得(不是很必要的情况就定义为整数)
先说整数:
整数可以由二进制,八进制(0<但是在严格模式下是微小的>),十进制,十六进制(0x)表示。
let number1 = 070; //八进制的56
let number2 = 079; //无效八进制,会被当做79
let number3 = 08; //无效八进制,会被当做8
let number4 = 0xA; //十六进制10
let number5 = 0x1f; //十六进制31
浮点数当小数点后面全为0时会自动转换为整数处理,科学计数法由e表示
千万注意在JavaScript中0.1+0.2 = 0.30000000000000004而不是0.3所以不要测试特定的浮点值
let number1 = 1.68;
let number2 = 0.1356;
let number3 = .123;
let number4 = 1.0; //当成1处理
还有一个特殊的数值叫NaN即不是数值(Not a Number)用于表示本来要返回数值的操作失败了。
0/0; //NaN
涉及NaN的所有操作始终返回NAN。 注意我不是我即NaN不等于NaN
console.log(NaN == NaN); //false
这里有一个方法isNaN()~~(是NaN吗?)~~用于帮我们判断一个值是不是数值。
不仅Boolean类型提供了与其他数据类型联动的方法,Number类型也能与其他数据类型联动Number():
| 数据类型 | |
|---|---|
| Boolean | true转换为1,false转换为0 |
| Number | 直接返回 |
| null | 0 |
| undefined | NaN |
| String | 空转换为0 |
| 只包含数值,返回数值 | |
| 除了数值还包含其他字符返回NAN |
let number1 = Number(true); //1
let number2 = Number(10); //10
let number3 = Number(""); //0
let number4 = Number(null); //0
let number5 = Number(undefined); //NaN
let number6 = Number("001"); //1
不仅仅是Number(),parseInt()也可以进行转换并且更为常用
let Number1 = parseInt(true); //NaN
let Number2 = parseInt(10); //10
let Number3 = parseInt(""); //NaN
let Number4 = parseInt(null); //NaN
let Number5 = parseInt(undefined); //NaN
let Number6 = parseInt("001"); //1
可以看出,Number()与parseInt()对于一些类型的转换还是有些许区别的。
此外parseInt()对于Number类型的转化也是杠杠的:
let number1 = parseInt("0xA"); //10为十六进制数
let number2 = parseInt("123木头人"); //123
let number3 = parseInt(3.14159) //3 碰到非数值字符会停止
//也可以接收两个参数,第二个参数指进制数
let number4 = parseInt("A",16); //10
let number5 = parseInt("10",2); //2 按二进制解析10
let number6 = parseInt("10",8); //8 按八进制解析10
不仅仅有parseInt(),parseFloat()也同样存在,但是parseFloat()只能解析十进制值,因此只传一个参数就可以了。
BIgInt
数字类型number无法表示大于2的53次方减1的和负2的53次方减1的数,而使用BigInt则可以表示任意长度的整数,有两种声明方式:
let big1 = 123123123123123123123123123123n; //以n结尾
let big2 = BigInt("123123123123123123123123123123"); //使用BigInt函数
String
字符串数据类型,可以用单引号(')双引号(")反引号(`)(好东西模板字符串)表示,但开头和结尾的引号必须是同一种引号。
let name = 'WDY';
let name = "WDY";
let name = `WDY`;
字符字面量
| 字面量 | 含义 |
|---|---|
| \n | 换行 |
| \t | 制表 |
| \b | 退格 |
| \r | 回车 |
| \f | 换页 |
| \ | 反斜杠 |
| ' | 单引号 |
模板字面量
模板字面量作为ES6中的新功能,使得我们在对变量的组合上更加得心应手:
let Name = "WDY";
let Age = "10000";
let My = "姓名:" + Name + "年龄:" + Age;
console.log(My); //姓名:WDY年龄:10000
let MMy = `姓名:${Name}年龄:${Age}`;
console.log(MMy); //姓名:WDY年龄:10000
同时模板字面量保留了换行字符,可以跨行定义字符串:
let My = `Name:WDY
Age:10000`;
console.log(My);
/**Name:WDY
Age:10000**/;
直接跨行输入即可而不需要输入\n,在使用模板字面量时里面的空格也会存在,因此记得注意格式的美观。
模板字面量标签函数
模板字面量支持定义标签函数(标签函数本身是一个常规函数),通过标签函数可以自定义其插值行为:
const Name = "WDY";
const age = "10000";
function get(strings, aVal, bVal, sumVal) {
console.log(strings);
console.log(aVal);
console.log(bVal);
console.log(sumVal);
return 'ok';
}
let my = `姓名:${Name},年龄:${age}`;
let My = get`姓名:${Name},年龄:${age}`;
console.log(my);
console.log(My);
/**[ '姓名:', ',年龄:', '' ]
WDY
10000
姓名:WDY,年龄:10000
ok
**/
在标签函数中参数的数量是可变的,所以使用剩余操作符(后面我们会提到)会更加方便:
const Name = "WDY";
const age = "10000";
function get(strings,...expressions){
console.log(strings);
for(const expression of expressions){
console.log(expression);
}
return ok;
}
let my = `姓名:${Name},年龄:${age}`;
let My = get`姓名:${Name},年龄:${age}`;
console.log(my);
console.log(My);
/**[ '姓名:', ',年龄:', '' ]
WDY
10000
姓名:WDY,年龄:10000
ok
**/
如果想直接得到拼接好的字符串:
let Name = "WDY";
let Age = "Age";
function get(strings,...expressions){
return strings[0] + expressions.map((e,i) => `${e}${strings[i+1]}`).join('');
}
let my = `姓名:${Name},年龄:${Age}`;
let My = get`姓名:${Name},年龄:${Age}`;
console.log(My); //姓名:WDY,年龄:Age
console.log(my); //姓名:WDY,年龄:Age
原始字符串
String.raw顾名思义即直接获取生的原始的字符串:
即不会转义。
Symbol
Symbol也是ES6的新增的一种数据类型,用来确保对象属性唯一标识通过Symbol()返回。
Object
说完7种简单数据,我们来聊一聊第八种数据结构——复杂数据结构(Object)
首先我们要知道对象类型为引用型 (见上次)
ECMA-262将对象定义为一组属性的无序集合,一个属性就是一个键值对,我们可以将其理解为一个容器,里面存放有各种东西(即其属性),我们有两种方法用来创建对象:
//First
let my = new Object(); //创建一个容器
my.Name = "WDY"; //写入属性
my.age = "10000";
my.MY = function(){
console.log(`${this.Name}+${this.age}`);
}
my["like eat"] = true; //多字词也可成为属性,这时我们需要将其用方括号括起来
//Second
let my = {
Name:"WDY",
age:10000,
My:function(){
console.log(`${this.Name}+${this.age}`);
}
"like eat":true //多字词也可成为属性,这时我们需要将其用引号引起来
}
语法糖来喽:
1.属性值简写:
let Name = "WDY";
let my = {
Name:Name
}
console.log(my.Name); //WDY
//变身
let Name = "WDY";
let my = {
Name
}
console.log(my.Name); //WDY 如果存在同名变量则成为属性,如果不存在则报错
2.方法名简写:
let my = {
sayName:function(Name){
console.log(`${Name}`);
}
};
my.sayName("WDY"); //WDY
//变身
let my = {
sayName(Name){
console.log(`${Name}`);
}
};
my.sayName("WDY"); //WDY
3.可计算属性(动态命名属性):
let Name = "Name";
let Age = "age";
let my = {};
my[Name] = "WDY"
my[Age] = 10000;
console.log(my); //{ Name: 'WDY', age: 100000 }
将简写方法名与计算属性结合起来:
let Method = "sayName";
let person = {
[Method](Name){
console.log(`${Name}`)
}
}
person.sayName("WDY"); //WDY
属性
接下来,我们将对对象的属性进行更深入的了解。对象的属性分为数据属性和访问器属性两种:
1.数据属性:
数据属性包含一个保存数据值的位置。值从这个地方进行读写,数据属性有4个特性:
| 描述符 | 特性 |
|---|---|
| [[Configurable]] (可否配置) | 表示该属性是否可以通过delete删除并重新定义以及是否可以修改其特性,默认true |
| [[Enumerable]](可否枚举) | 表示该属性是否可以通过for-in循环返回,默认true |
| [[Writable]](可否修改) | 表示该属性是否可以被修改,默认true |
| [[Value]] | 实际属性的值,默认undefined |
举个栗子:
let my = {
name:"WDY"; //[[Value]]被设定
}
既然属性都有默认值,那么我们怎样修改呢?Object.defineProperty()方法,该方法接收三个参数(要添加属性的对象,属性名称,描述符):
let my = {};
Object.defineProperty(my,"Name",{
writeable:false,
value:"WDY"
});
console.log(my.Name); //WDY
my.Name = "wdy";
console.log(my.Name); //WDY
//严格模式下,给my.Name赋值会抛出错误
2.访问器属性:
访问器属性不包含数据值:
| 描述符 | 行为 |
|---|---|
| [[Configurable]] | 表示该属性是否可以通过delete删除并重新定义以及是否可以修改其特性,默认true |
| [[Enumerable]] | 表示该属性是否可以通过for-in循环返回,默认true |
| [[Get]] | 获取函数,读取属性时调用,默认undefined |
| [[Set]] | 设置函数,写入属性时调用,默认undefined |
栗子:
let my = {
Name:"wdy",
Age:10000
};
Object.defineProperty(my,"age",{
get(){
return this.Age;
},
set(trueAge){
if(trueAge > 100){
this.Age = trueAge;
this.Name = "WDY";
}else{
this.Name = this.Name;
}
}
});
my.age = 99;
console.log(my.Name); //wdy
//变身
let my = {
Name:"wdy",
Age:10000,
get age(){
return this.Age;
},
set age(trueAge){
if(trueAge > 100){
this.Age = trueAge;
this.Name = "WDY";
}else{
this.Name = this.Name;
}
}
};
my.age = 99;
console.log(my.Name); //wdy
//区别在于一个调用Object.defineProperty(),一个没有。
这些属性可以单个修改,那么可以一次性修改多个吗?可以:Object.defineProperties()方法:
let my = {};
Object.defineProperties(my,{
Name:{
writable:false
value:"WDY"
},
age:{
value:10000;
}
})
两级反转---Object.getOwnPropertyDescription()方法,该方法可以取得指定属性的属性描述符:
let my = {};
Object.defineProperties(my,{
Name:{
writable:false,
value:"WDY"
},
age:{
value:10000
}
});
let message = Object.getOwnPropertyDescription(my,"Name");
console.log(message.writable); //false
console.log(message.value); //WDY
console.log(typeof message.get); //undefined
一劳永逸----Object.getOwnPropertyDescriptors()方法,获取所有:
let my = {};
Object.defineProperties(my,{
Name:{
writable:false,
value:"WDY"
},
age:{
value:10000
}
});
console.log(Object.getOwnPropertyDescriptors(my));
/** {
Name: {
value: 'WDY',
writable: false,
enumerable: false,
configurable: false
},
age: {
value: 10000,
writable: false,
enumerable: false,
configurable: false
}
}**/
对象解构
在一条语句中实现一个或者多个赋值操作:
1.解构
let my = {
Name:"WDY",
Age:10000
};
console.log(my.Name); //WDY
console.log(my.Age); //10000
//变身
let my = {
Name:"WDY",
Age:10000
};
let {Name:myName,Age:myAge} = my;
console.log(myName); //WDY
console.log(myAge); //10000
//再变
let my = {
Name:"WDY",
Age:10000
};
let {Name,Age} = my;
console.log(Name); //WDY
console.log(Age); //10000
//再再变
let my = {
Name:"WDY",
Age:10000
};
let {Name,Age} = my;
console.log(Name); //WDY
console.log(Age); //10000
console.log(my.Sex); //undefined---该属性不存在
//再变变
let my = {
Name:"WDY",
Age:10000
};
let {Name,Age,Sex="Guess"} = my;
console.log(Name); //WDY
console.log(Age); //10000
console.log(Sex); //Guess
2.部分解构: 如果要对多个属性结构赋值,开始的成功而后面的出错则解构也会完成一部分:
let my = {
name:"WDY",
Age:10000
};
let myName,myAge,mySex;
try{
({Name:myName,Sex:mySex,Age:myAge} = my);
}catch(e){}
console.log(myName,myAge,mySex);
//WDY,undefined,undefined 一旦出错就会停止赋值但前面成功的仍会保留
3.嵌套解构:
let my = {
Name:"WDY",
age:10000,
hobbies:{
eat:true,
sport:true
}
};
let anotherMy = {};
({
Name:anotherMy.Name,
Age:anotherMy.Age,
hobbies:anotherMy.hobbies
} = my);
anotherMy.hobbies.sport = false;
console.log(my);
//{ Name: 'WDY', age: 10000, hobbies: { eat: true, sport: false } }
console.log(anotherMy);
//{ Name: 'WDY', age: 10000, hobbies: { eat: true, sport: false } }
/**为什么my中的sport也变成了false呢?请见20220304的引用型变量**/
创建对象
上面我们说了创建对象的两种方法,但是,当我们需要大量创建属性类型相同的对象时有什么更好的方法呢?(比如学生管理系统)
1.工厂模式
顾名思义即类似于流水线般创建对象:
function student(name,age,card){
let s = new Object();
s.name = name;
s.age = age;
s.card = card;
s.who = function(){
console.log(`${name}${age}${card}`);
};
return s;
}
let s1 = student("小明",12,20220001);
let s2 = student("小红",12,20220002);
2.构造函数模式
构造函数也是函数 (后面我们详解),它与普通函数唯一的区别在于调用方式不同,任何函数使用new操作符调用就是构造函数(注意函数首字母大写):
function Student(name,age,card){
this.name = name;
this.age = age;
this.card = card;
this.who = function(){
console.log(`${name}${age}${card}`)
}
}
let s1 = student("小明",12,20220001);
let s2 = student("小红",12,20220002);
console.log(s1.who == s2.who); //false
/**相较于工厂模式,构造函数模式没有显式创建对象而且属性和方法直接赋值给了this**/
使用构造函数,定义的方法会在每个实例上都创建一遍因此s1和s2的who是各自的方法
3.原型模式 (敲重点&超重点)
每个函数都会创建一个prototype属性,这个属性是一个对象。使用原型对象的好处就是在其上面定义的属性和方法可以被对象实例共享 (这是构造函数所不能的)
无论何时,只要创建函数,这个函数就会带有prototype属性,这个属性指向原型对象,此时原型对象会生成一个constructor属性,这个属性指向哪里呢?指向原型对象 (不好懂没关系,上代码,上图!) 各位客官看好啦:
function My(){}
My.prototype.name = "WDY";
My.prototype.age = 10000;
My.prototype.hobby = "eat";
My.prototype.who = function(){
console.log(this.Name);
};
let my1 = new My();
my1.who();
let my2 = new My();
my2.who();
console.log(my1.who == my2.who); //true
原型层级:上面例子中,my1和my2中并没有who方法,但是却能成功调用其原因是:在通过对象访问属性时,会按照这个属性的名称开始搜索,搜索开始于实例本身即my1那个容器,如果找到了,则返回对应的值,如果没找到,那么会沿指针进入原型对象,如果找到,则返回对应的值。如果是在原型中找到的则:该值只能访问不能修改,但是可以添加该属性:
function My(){}
My.prototype.name = "WDY";
My.prototype.age = 10000;
My.prototype.hobby = "eat";
My.prototype.who = function(){
console.log(this.Name);
};
let my1 = new My();
let my2 = new My();
my1.name = "wdy";
console.log(my1.name); //wdy
console.log(my2.name); //WDY
delete my1.name;
console.log(my1.name); //WDY
my1有了自己的属性(也称为遮蔽),自然就不会再去原型里面寻找了,但是如果将该属性删掉,就又到了原型中。
Object类型中Object.getPrototypeOf()方法,返回参数内部特性[[prototype]]的值:
console.log(Object.getPrototypeOf(my1) == My.prototype); //true
console.log(Object.getPrototypeOf(my1.name)); //WDY
hasOwnProperty()方法,用于确定某个属性是在实例上还是在原型对象上: in操作符,用于确定实例是否可以访问到该属性:
function My(){}
My.prototype.name = "WDY";
My.prototype.age = 10000;
My.prototype.hobby = "eat";
My.prototype.who = function(){
console.log(this.Name);
};
let my1 = new My();
let my2 = new My();
console.log(my1.hasOwnProperty("name")); //false
console.log("name" in my1); //true
my1.name = "wdy";
console.log(my1.hasOwnProperty("name")); //true
console.log("name" in my1); //true
delete my1.name;
console.log(my1.hasOwnProperty("name")); //false
console.log("name" in my1); //true
原型是动态的
由于从原型上搜索值的过程是动态的,所以即使实例在原型前已经存在,任何时候对原型的修改也会在实例上表现出来:
let my = new My();
function My(){}
My.prototype.name = "WDY";
My.prototype.age = 10000;
My.prototype.hobby = "eat";
My.prototype.who = function(){
console.log(this.Name);
};
console.log(my.name); //WDY
上述虽然体现了原型的动态性,但是其条件是没有将原型重写 (当然原型是可以重写的)
function My(){};
let my = new My();
My.prototype = {
constructor:My,
name : "WDY",
age :10000,
hobby:eat,
who(){
console.log(whis.name);
}
}
my.who(); //错误
重写原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
当然,原型模式也不是完美无缺的,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同时值,还有一个更大的问题就是其共享性,即一个变全部变,一般来说,不同的实例应该有属于自己空间,而使用原型使他们指向了同一片空间。
继承
作为面向对象编程的语言,JS也支持继承
原型链继承
function Animal(name){
this.name = name;
this.property = true;
}
Animal.prototype.eat = function(){
console.log(this.name+'正在吃');
}
function Cat(){
}
Cat.prototype = new Animal('maomao');
Cat.prototype.name = '猫猫';
Cat.prototype.sleep = function(){
console.log('猫猫在睡觉');
}
let cat = new Cat();
console.log(cat.name);
console.log(cat.eat());
console.log(cat.sleep());
console.log(cat instanceof Animal);
console.log(cat instanceof Cat);
/*
猫猫
猫猫正在吃
undefined
猫猫在睡觉
undefined
true
true
函数为使用return时调用会返回undefined
*/
原型继承有两个缺陷:
1.子类可以访问父类的所有
2.子类实例化时无法给父类传参
盗用构造函数
function Animal(name){
this.name = name;
this.property = true;
this.play = function(){
console.log(this.name+'在玩');
}
}
Animal.prototype.eat = function(){
console.log(this.name+'正在吃');
}
function Cat(name){
Animal.call(this);
this.name = name;
}
const cat = new Cat('miaomiao');
console.log(cat.name);
console.log(cat.play());
console.log(cat instanceof Animal);
console.log(cat instanceof Cat);
/*
miaomiao
miaomiao在玩
undefined
false
true
*/
const cat1 = new Cat('MiaoMiao');
console.log(cat1.name);
console.log(cat1.play());
/*
MiaoMiao
MiaoMiao在玩
undefined
*/
1.子类不能访问父类原型(修改值不会影响别的实例)
2.子类实例化时可以向父类传递参数
3.可以实现多继承(call多个)
盗用构造函数也有缺陷:
1.必须在构造函数中定义方法
2.子类不能访问父类原型
组合继承
function Animal(name){
this.name = name;
this.property = true;
this.play = function(){
console.log(this.name+'在玩');
}
}
Animal.prototype.eat = function(){
console.log(this.name+'正在吃');
}
function Cat(name="MAOMAO"){
Animal.call(this);
this.name = name;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
const cat1 = new Cat('miaomiao');
console.log(cat1.name);
console.log(cat1.play());
console.log(cat1 instanceof Animal);
console.log(cat1 instanceof Cat);
const cat2 = new Cat('MiaoMiao');
console.log(cat2.name);
console.log(cat2.play());
/*
miaomiao
miaomiao正在吃
undefined
true
true
MiaoMiao
MiaoMiao正在吃 在盗用构造函数中,eat方法未定义,而组合继承中,可以使用
undefined
*/
组合式继承弥补了原型链继承和盗用构造函数继承的不足,使得子类不共享同一片空间,也使得子类可以传递参数。
但是在组合式继承中,父类的构造函数会被调用两次,创建子类原型时和子类构造函数调用时,会影响效率
原型式继承
let person = {
name:"A",
friends:['B','C']
}
let anotherPerson = Object.create(person);
anotherPerson.name = "B";
anotherPerson.friends.push('a');
console.log(person.name)
console.log(person.friends);
console.log(anotherPerson.name);
console.log(anotherPerson.friends);
/*
A
[ 'B', 'C', 'a' ]
B
[ 'B', 'C', 'a' ]
*/
原型式继承适用于不需单独创建构造函数,但仍想要在对象间共享信息的场合。
调用了Object.create()方法,这个方法接收两个参数:作为新对象原型的对象,给新对象定义额外属性的对象(可选)
寄生式继承
let person = {
name:"A",
friends:['B','C']
}
function createAnotherPerson(original){
let clone = Object.create(original);
clone.getFriends = function(){
return this.friends;
}
return clone;
}
let anotherPerson = createAnotherPerson(person);
console.log(anotherPerson.getFriends())
使用原型式继承可以得到目标对象的浅拷贝,将浅拷贝能力加强就是寄生式继承了
寄生式组合继承
function inheritPrototype(parent,child){
let prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
} //核心
function Parent(){
this.name = "A";
this.friend = ["B","C"]
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
Parent.call(this);
this.age = 10000;
}
inheritPrototype(Parent,Child);
Child.prototype.getAge = function(){
return this.age;
}
let p = new Child();
console.log(p);
console.log(p.getAge());
console.log(p.getName());
这个图是我对寄生式组合继承的理解
寄生式组合继承被认为是实现基于类型继承的最有效的方式**对父类构造函数只进行了一次调用
一些内置对象
JavaScript中,除了我们刚才说的自定义对象,还有许多内置对象:
Date
Date是一个构造函数~~(方法也属于对象)~~需要先实例化在使用。
let date1 = new Date(); //2022-04-01T07:47:35.369Z
//空参返回当前时间
let date2 = new Date('2022/04/1 15:00:00'); //2022-04-01T07:00:00.000Z
let date3 = new Date('2022/04/1'); //2022-03-31T16:00:00.000Z
let date4 = new Date('2022-04-1'); //2022-03-31T16:00:00.000Z
let date5 = new Date('Fri April 1 2022 15:00:00 GMT+0800 (中国标准时间)');
//2022-04-01T07:00:00.000Z
let date6 = new Date(2022, 4, 1); //2022-04-30T16:00:00.000Z
let date7 = new Date(2022, 4, 1, 15, 45, 58); //2022-05-01T07:45:58.000Z
//从0开始,所以第四个是5月
let date8 = new Date(2022, 04, 12, 16, 20, 59); //2022-05-12T08:20:59.000Z
除了获取完整时间,Date还提供了其他方法(以下为常用):
| 方法名 | 含义 | 备注 |
|---|---|---|
| getFullYear() | 获取年份 | |
| getMonth() | 获取月: 0-11 | 0代表一月 |
| getDate() | 获取日:1-31 | 获取的是几号 |
| getDay() | 获取星期:0-6 | 0代表周日,1代表周一 |
| getHours() | 获取小时:0-23 | |
| getMinutes() | 获取分钟:0-59 | |
| getSeconds() | 获取秒:0-59 | |
| getMilliseconds() | 获取毫秒 | 1s = 1000ms |
Math
与Date不同Math不是构造函数,不需要使用new实例,直接可调用
| 方法 | 描述 |
|---|---|
| Math.abs() | 返回绝对值 |
| Math.random() | 生成0-1之间的随机浮点数 |
| Math.floor() | 向下取整(往小取值) |
| Math.ceil() | 向上取整(往大取值) |
| Math.round() | 四舍五入取整(正数四舍五入,负数五舍六入) |
| Math.max(x, y, z) | 返回多个数中的最大值 |
| Math.min(x, y, z) | 返回多个数中的最小值 |
| Math.pow(x,y) | 乘方:返回 x 的 y 次幂 |
| Math.sqrt() | 开方:对一个数进行开方运算 |
判断数据类型
我们现在已经了解了各种数据类型,如果我们想要知道某个变量是那个类型的数据时应该怎么办呢?
typeof
注意typeof(null,数组)返回的是object,因为null被认为是一个空对象引用。
instanceof
instanceof是为对象而生的,可以正确的判断对象的类型
注意,instanceof只能判断引用型数据类型。
constructor
执行上下文&作用域链&this
在研究函数之前,我们先了解一下执行上下文,作用域链,this这三个概念
执行上下文
JS代码在执行之前,JS引擎首先会创建对应的执行上下文,在JS中,执行上下文共有三种:全局执行上下文,函数执行上下文,eval执行上下文(暂时不提)。
全局执行上下文
全局上下文是最外层的上下文,一般有浏览器的创建,就是我们常说的window对象。还记得var吗?使用var定义的全局变量和函数都会成为window对象的属性和方法而使用let和const则不会。
函数执行上下文
每个函数被调用时都会创建执行上下文,同一个函数被多次调用就会多次创建。在函数执行上下文被创建之后,他们会被存放在执行上下文堆栈~~(堆栈是一个先进后出的结构)~~中,在函数执行完之后,该函数的执行上下文会被弹出栈
function function1(){
function2();
console.log("1");
}
function function2(){
function3();
console.log("2");
}
function function3(){
console.log("3");
}
function1(); //3 2 1
执行函数function1,function1创建执行上下文并压入栈中,function1调用function2,function2创建执行上下文并压入栈中,function2调用function3,function3创建执行上下文并压入栈中,此时function3位于栈顶,因此先执行function3,再执行function2,最后执行function1。最后仅留下全局执行上下文。
作用域链
再了解作用域链之前请先了解作用域~~(见第一章)~~。
当我们调用一个变量时,如果在当前作用域中没有找到该值,就会向更高一级作用域去找,这个过程就形成了作用域链~~(我将其与原型放一起理解)~~。
let a = 10;
function fn1(){
return a+10;
}
console.log(fn1()); //20
let b = 10;
function ffn1(){
function ffn2(){
console.log(b+10);
}
return ffn2;
}
ffn1()(); //20
this
var a = 10;
function fn(){
console.log(this.a);
}
fn(); //undefined
在浏览器中,他们的上级是window对象,所以调用时会返回10。
let my={
name:"WDY",
age:10000,
mmy:function(){
console.log(this.name);
console.log(this.age);
}
}
my.mmy(); //WDY 10000
通过对象调用的函数的this很好理解,mmy函数的所有者是对象my因此函数的this指向了my。
let my={
name:"WDY",
age:10000,
mmy:function(){
console.log(this.name);
console.log(this.age);
}
}
let My={
name:"wdy",
age:1000,
}
My.mmy = my.mmy;
My.mmy(); //wdy 1000
这个栗子就很鲜明了,虽然My的mmy方法是从my来的,但调用mmy的是My,因此这里的this指向了My 。
function My(){
this.name = "WDY";
}
let my = new My();
console.log(my.name); //WDY
对于构造函数,this指向实例化对象。
函数
数据类型说完,我们来聊一聊函数。JavaScript中函数是属于对象的,我们仍旧从函数的声明与调用开始说起:
声明及调用
//函数声明
function my(name,age){
return `姓名:${name},年龄:${age}`;
}
//函数表达式
let my = function(name,age){
return `姓名:${name},年龄:${age}`;
}
//构造函数
let my = new Function("name","age","return `姓名:${name},年龄:${age}`");
//不推荐使用构造函数,这样声明函数代码会被解释两次,第一次:将它当做常规的代码,第二次:解释传给构造函数的参数
//箭头函数
let my = (name,age) => return `姓名:${name},年龄:${age}`;
my("WDY",10000);//调用函数
那么使用函数声明和函数表达式声明函数有什么区别呢?函数声明会在执行其他代码之前先被读取并添加到执行上下文~~(请见下文)~~中,类似于var造成的变量提升,这里是函数声明提升:
console.log(my("WDY",10000)); //姓名:WDY,年龄:10000
function my(name,age){
return `姓名:${name},年龄:${age}`;
}
console.log(my("WDY",10000)); //报错
let my = function(name,age){
return `姓名:${name},年龄:${age}`;
}
参数
首先,我们要有实参和形参的概念。其次,在JavaScript的函数中,参数并不是那么严格:
//这里的name和age就是形参
function my(name,age){
return `姓名:${name},年龄:${age}`
}
//WDY就是实参
//虽然没有传age,但是仍能正常运行
console.log(my("WDY")); //姓名:WDY,年龄:undefined
同时,我们也可以指定默认参数,该默认参数会在未给其传参时调用:
function my(name,age=10000){
return `姓名:${name},年龄:${age}`
}
console.log(my("WDY")); //姓名:WDY,年龄:10000
console.log(my("WDY",20000)); //姓名:WDY,年龄:20000
在参数方面,arguments对象发挥着重要作用。arguments对象是一个类数组对象~~(类数组就是不完全具有数组的方法pop,push等)~~,包含调用函数的所有参数也可以理解为封装实参的对象。
function myy(){
return `姓名:${arguments[0]},年龄:${arguments[1]}`;
}
console.log(myy("WDY",10000)); //姓名:WDY,年龄:10000
function myy(){
return `姓名:${arguments[1]},年龄:${arguments[2]}`;
}
console.log(myy("WDY",10000,20000)) //姓名:10000,年龄:20000
通过arguments.length()可以得到参数的个数。
除了封装实参,arguments的callee属性也是十分重要的,arguments.callee用于返回正在执行的函数。:
function factorial(n){
if(n <= 1){
return 1;
}else{
return n*factorial(n-1);
}
}
function factorial(n){
if(n <= 1){
return 1;
}else{
return n*reguments.callee(n-1);
}
}
但是从这个例子来看arguments.callee并没有什么过人之处不是吗?请看这个栗子:
function factorial(n){
if(n <= 1){
return 1;
}else{
return n*factorial(n-1);
}
}
let trueFactorial = factorial;
factorial = function(){
return 0;
}
console.log(trueFactorial(5)); //0
function factorial(n){
if(n <= 1){
return 1;
}else{
return n*arguments.callee(n-1);
}
}
let trueFactorial = factorial;
factorial = function(){
return 0;
}
console.log(trueFactorial(5)); //120
这个栗子很清晰的体现了arguments.callee的作用,严格模式下的arguments.callee是不能访问的。
属性
既然函数属于对象,那么它肯定有属于自己的属性。每个函数都有两个属性:length(形参个数)和prototype(原型)。
方法
函数不仅有自己的属性,他还有自己的方法:apply(),call()这两个方法都可以改变this的指向。
apply()
//不含参数
let my={
name:"wdy",
age:1000,
my:function(){
console.log(`${this.name}${this.age}`);
}
}
let My={
name:"WDY",
age:10000
}
my.my.apply(My); //WDY10000
//含参数
let my={
name:"wdy",
age:1000,
my:function(name,age){
console.log(`${name}---${age}`);
}
}
let My={
}
my.my.apply(My,['WDY',10000]); //WDY---10000
my函数由my对象调用,但由于apply改变了this的指向使之指向My,因此my函数调用的便是My的属性。
call()
//不含参数
let my={
name:"wdy",
age:1000,
my:function(){
console.log(`${this.name}${this.age}`);
}
}
let My={
name:"WDY",
age:10000
}
my.my.call(My); //WDY10000
//含参数
let my={
name:"wdy",
age:1000,
my:function(name,age){
console.log(`${name}---${age}`);
}
}
let My={
}
my.my.call(My,"WDY",10000); //WDY---10000
bind()
//不含参数
let my={
name:"wdy",
age:1000,
my:function(){
console.log(`${this.name}${this.age}`);
}
}
let My={
name:"WDY",
age:10000
}
my.my.bind(My)(); //WDY10000
//含参数一
let my={
name:"wdy",
age:1000,
my:function(name,age){
console.log(`${name}---${age}`);
}
}
let My={
}
my.my.bind(My,"WDY",10000)(); //WDY---10000
//含参数二
let my={
name:"wdy",
age:1000,
my:function(name,age){
console.log(`${name}---${age}`);
}
}
let My={
}
my.my.bind(My,["WDY",10000])(); //WDY,10000---undefined
apply,call,bind三者都可以改变this的指向,但是bind与前两者略有差别(将函数返回)。
在传参方面也有区别
箭头函数
ES6中新增了箭头函数
let my = function(){
console.log("我是普通函数");
}
my(); //我是普通函数
let My = () => {console.log("我是箭头函数");}
My(); //我是箭头函数
//简写
let my = function(str){
console.log("我是普通函数"+str);
}
my("11111"); //我是普通函数11111
let My = (str) => {console.log("我是箭头函数"+str);}
My('22222'); //我是箭头函数22222
let Mmy = str => console.log("我是箭头函数"+str)
Mmy("33333"); //我是箭头函数33333
/*当参数只有一个时可以不加括号,当函数体只有一句时可以不加花括号*/
/*嵌入式函数与箭头函数绝配*/
箭头函数虽然简便但是也有许多与普通函数不同的地方:
this
在箭头函数中,this指向的是定义this的位置
const obj = {name:'WDY'};
function fn1(){
console.log(this);
return ()=>{
console.log(this);
}
}
fn1(); //两个this都是指向fn1的
const fn2 = fn1.call(obj);
fn2();
//{ name: 'WDY' }
//{ name: 'WDY' }
上面的代码中,由于构造函数是fn1定义的,所以this先指向了fn1,之后通过call改变了指向使其指向了obj,从而输出obj
上面代码中,由于对象不具有作用域,箭头函数相当于在window中产生的,所以this指向了window
箭头函数没有arguments,super,new.target,new**
匿名函数
使用函数表达式方法创建的函数也成为匿名函数(function后面没有自己的名字):
(function (){console.log("hhh")})()
//hhh
(function (){console.log("hhh")}())
//hhh
匿名函数无需调用,创建了就会立即执行。
高阶函数
当函数的参数或者返回值为函数时,我们将主体函数称为高阶函数
function fn1(a,b,c){
return a+b+c;
}
console.log(fn1(1,2,3)) ;
//6
function fn1(a){
return function fn2(b){
return function fn3(c){
return a+b+c;
}
}
}
console.log(fn1(1)(2)(3));
//6
function fn1(a,b,fn){
console.log(a+b);
fn();
}
fn1(1,2,function (){console.log('高阶函数')})
//6
//高阶函数
JS中有很多内置的高阶函数如Array中的map,filter,reduce等
闭包
闭包:访问另一个函数里的作用域:
function fn1(){
const a = 10;
return function fn2(){
console.log(a);
}
}
fn1()();
//10
类&面向对象
类是什么?
在JS中,我们可以把类理解为特殊的函数:
class My{
}
console.log(typeof My)
//function
类中也有prototype属性,原型也有一个constructor属性指向自身
声明及调用
//类声明
class My{}
//类表达式
const My = class {}
//实例化
let my = new My
/*
使用
*/
与函数不同,类定义不能变量提升。
类可以作为参数传递。
构造函数
在类中,要注意区分constructor是属性还是构造函数
使用new实例化会自动调用该类的构造函数,构造函数也可以不写,相当于空函数
class My{
constructor(){
console.log("我是构造函数");
}
}
let my = new My;
//我是构造函数
类实例化时,传入的参数会用作构造函数的参数:
class My{
constructor(name){
console.log(`姓名:${this,name}`)
}
}
let my = new My("WDY")
//姓名:WDY
//不传递参数时,后面的括号可以省略
调用类构造函数时,必须使用new操作符。
实例
使用new操作符实例化的成员叫做实例成员,实例成员之间互不干扰。
静态类方法
无需实例化对象即可调用
class My{
constructor(){
}
static my(){
console.log('我就是我')
}
}
My.my();
//我就是我
在静态成员中,this指向类自身
原型
类块中定义的方法被称为原型方法,由所有实例成员共享。而在类块中定义的所有内容都会定义在该类的原型上:
class My{
constructor(){
this.my = () => {console.log("我就是我")}
}
my(){
console.log("我还是我");
}
}
let my = new My;
my.my();
My.prototype.my();
/*
我就是我
我还是我
*/
class My{
constructor(){
}
my(){
console.log("我还是我");
}
}
let my = new My;
my.my();
//我还是我
继承
extends,JS中的继承属于单继承~~(即只可以有一个父类)~~类中继承的实现也是基于原型链的。
class Father{}
class Child extends Father{}
let a = new Child()
console.log(a instanceof Child)
console.log(a instanceof Father)
/*
true
true
*/
class Father{
constructor(){
console.log('我是Father')
}
}
class Child extends Father{}
let a = new Child()
//我是Father 类构造函数在实例化时会自动调用
class Father{
constructor(){
console.log('我是Father')
}
}
class Child extends Father{
constructor(){
console.log('我是Child')
//super()
}
}
let a = new Child()
/*
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
*/
/*
这是由于在继承中,子类必须调用super才能调用this或者从类构造函数中返回
*/
class Father{
constructor(){
console.log('我是Father')
}
}
class Child extends Father{
constructor(){
super()
console.log('我是Child')
}
}
let a = new Child()
/*
我是Father
我是Child
*/
super()
上述案例中谈到了super关键字:通过super可以引用父类原型仅限:类构造函数,实例方法,静态方法
- super只能在子类中使用
- 不能单独使用super
- 如果没有定义类构造函数,在实例化子类时会自动调用super()
- 调用super()会调用父类构造函数
- 在类构造函数中,不能在super前使用this
- 如果子类定义了构造函数,必须在其中调用super或者返回一个对象
抽象基类
抽象基类:一个类可以被其他类继承,但本身不能被实例化
class Father{
constructor(){
if(new.target === Father){
throw new Error('该类不能被实例化')
}
}
}
class Child extends Father{
constructor(){
super()
console.log('我是Child')
}
}
let a = new Child()
let b = new Father()
/*
我是Child
d:\New\Basic\basic11.js:4
throw new Error('该类不能被实例化')
^
Error: 该类不能被实例化
*/
JSON
JSON是JavaScript的严格子集,用来表示结构化数据,类似于(XML)。JSON并不属于JavaScript,它仅是一种数据格式,JSON也不仅仅能在JavaScript中使用,很多语言都可以解析JSON
在JSON中必须使用双引号
JSON支持三种数据类型:简单值,数组,对象。但这并不是JSON的数据类型,JSON中没有变量,同时也就不具有变量声明了:
{
"my":{
"name":"A",
"age":10000
},
"My":["a",1000]
}
[
{
"name":"A",
"age":10000
},
["a",1000]
]
解析与序列化
stringify()
let my = {
name:"WDY",
age:10000,
hobby:["sport","sleep","eat"]
}
let mmy = JSON.stringify(my);
console.log(mmy)
//{"name":"WDY","age":10000,"hobby":["sport","sleep","eat"]}
let my = {
name:"WDY",
age:10000,
hobby:["sport","sleep","eat"]
}
let mmy = JSON.stringify(my,["name","age"]);
console.log(mmy)
//{"name":"WDY","age":10000}
/*
给stringify传第二个参数,即筛选要保留的内容
*/
let my = {
name:"WDY",
age:10000,
hobby:["sport","sleep","eat"],
toJSON:function(){
return this.name
}
}
let mmy = JSON.stringify(my);
console.log(mmy)
//"WDY"
/*
设置toJSON方法设定stringify的返回值
*/
将JavaScript序列化为JSON字符串
parse()
let my = {
name:"WDY",
age:10000,
hobby:["sport","sleep","eat"]
}
let mmy = JSON.stringify(my);
let My = JSON.parse(mmy)
console.log(My)
//{ name: 'WDY', age: 10000, hobby: [ 'sport', 'sleep', 'eat' ] }
异步
正则表达式
// (1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
// (3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g;
// (4)手机号码正则
var regex = /^1[34578]\d{9}$/g;
// (5)用户名正则
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
DOM
JavaScript DOM
BOM(Browser Object Model)
JavaScript中的最最基本的对象要数window对象了,window对象本身对应着浏览器窗口,而这个对象的属性和方法通常称为BOM(浏览器对象模型)
DOM(Document Object Model)
DOM树
那么DOM是什么呢?文档对象模型。DOM把一份文档解析为一棵树,即我们常说的DOM树。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> DOM </title>
</head>
<body>
<h1>Let's learn DOM!</h1>
<p>Are you ready?</p>
<ul id="DOM">
<li>D</li>
<li>O</li>
<li>M</li>
</ul>
</body>
</html>
接下来,就让我们用这个小案例敲开DOM的大门:
将上述代码抽象为DOM树即为上图,我们可以很清楚的看出html为树根即根元素,
之后html的两个分支meta与body为html的子元素而他们两个之间则是并列关系也可称为兄弟关系,再往下即body的三个子元素h1 p和ul,继续往下ul里面又包含三个子元素li li li,这样,我们就将这个代码的DOM树分析完成了。
节点
了解完DOM树,我们来了解一下节点,之后我们便可以通过节点操作DOM树
节点顾名思义即小节的点,也可以理解为连接点,将DOM树连接起来的东西。
DOM节点共有12种:
***元素节点,文本节点,属性节点,***CDATA节点,实体引用名称节点,实体名称节点,处理指令节点,注释节点,文档节点,文档类型节点,文档片段节点,DTD声明节点。
以上12中节点中,我们将对前三种进行说明:
| 节点类型 | 说明 |
|---|---|
| 元素节点 | html的标签就是元素节点 |
| 文本节点 | 包含内容的元素节点又被称为文本节点就像上面的h1,p |
| 属性节点 | 标签里面的属性就是属性节点,用于对元素做出更准确的描述 |
操作
了解完DOM树与节点,我们就可以尝试着通过节点操作DOM树了:
| 函数 | 作用 |
|---|---|
| getElementById() | 获取元素节点,通过id属性获取 |
| getElementsByTagName() | 获取元素节点,将要获取的元素节点存储在一个数组中。 |
| getElementsByClassName() | 获取元素节点,通过class属性获取 |
| createElement() | 创建元素节点 |
| createTextNode() | 填充节点内容。当创建一个元素节点并将其添加进DOM树后,可对其进行内容填充 |
以上的函数通过document调用,而下面的函数则是通过元素节点对象调用的
| 函数 | 作用 |
|---|---|
| getAttribute() | 获取元素节点的属性 |
| setAttribute() | 更改元素节点的属性 |
| appendChild() | 让参数节点成为现有节点的子节点 |
| insertBefore() | 在节点前插入 |
部分内容参考自:《JavaScript高级编程设计》