20220303
学习JavaScript首当其冲的就是了解其数据类型与变量声明,今天分享一下我对于变量声明的理解
(文中一些小的注释可能会令大家觉得没有必要,但是对于一些童鞋可能确实有用<比如我这样的小菜鸡>小声bb)
变量声明,作用域
众所周知,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!
20220304(20220307完善)
昨天总结了js中的三种变量声明方式,今天分享一下我对于数据类型的理解。
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
对象作为一种复杂数据类型我们后面聊。
typeof
我们现在已经了解了各种数据类型,如果我们想要知道某个变量是那个类型的数据时应该怎么办呢?typeof操作符:
const testeString = "Hello";
const testNumber = 100;
//typeof 是操作符而不是函数,因此不需要参数(但可以使用)
console.log(typeof(testeString)); //string
console.log(typeof testeString); //string
console.log(typeof testNumber); //number
注意typeof(null)返回的是object,因为null被认为是一个空对象引用。
20220311
上回说到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(); //错误
重写原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
当然,原型模式也不是完美无缺的,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同时值,还有一个更大的问题就是其共享性,即一个变全部变,一般来说,不同的实例应该有属于自己空间,而使用原型使他们指向了同一片空间。