js高级
var 声明
var 声明的变量没有块级作用域(ES5中的作用域:全局和局部)
1、可以先使用,后声明
2、可以重复定义同一个变量,逻辑错误,第二次应该是修改变量,而不是定义
3、var用在for循环条件中,造成for 循环的污染的问题
4、var 声明的变量没有块级作用域(ES5中的作用域:全局和局部)
let声明
语法:
let 变量 = 值;
ES6中为了统一并提高代码的安全性,引入了let关键字来代替var声明变量
特点:
1、let声明的变量没有预解析,不会有变量提升
2、同一作用域let不可以重复定义同一个变量
3、let用在for循环条件中,不会造成for 循环的污染的问题
4、let声明的变量有块级作用域(ES6中的作用域:全局和局部还有块级作用域)
块级作用域
块指的是代码块,一对 {}
之间就是一个代码块。
变量的有效范围只在这对 大括号 之间,就是块级作用域
语法——作用域
var的声明
var btns = document.querySelectorAll('button')
for(var i = 0; i < btns.length; i++){
btns[i].onclick = function(){
console.log(i)
}
}
console.log(i) //btns.length
let声明
let btns = document.querySelectorAll('button')
for(let i = 0; i < btns.length; i++){
btns[i].onclick = function(){
console.log(i)
}
}
console.log(i) //没有定义
const关键字
ES6中,const关键字来声明一个只读的常量
常量具备以下特点:
- 一旦声明就必须赋值
- 一旦赋值就不能修改
- 常量的作用域和let声明的变量作用域一样 块级作用域
- 没有预解析
- 引用类型的值可以修改
语法:
const 常量名 = 值;
模板字符串
语法:
`固定字符${变量或者表达式}`
例如:
let name = '狗蛋',age = 12,gender = '男'
let str = "大家好,我叫" + name + ",今年" + age + "岁了,我是一个" + gender + "孩子"
特点:
- 在模板字符串中,可以解析
${}
之间的变量或者表达式 - 在整个字符串中允许换行
解构语法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。
对象解构
let obj = {
name: "luowowo",
age:11,
email:"luowowo@163.com"
};
// 取出所有属性并赋值:
// let name = obj.name;
// let age = obj.age;
// let email = obj.email;
// 现在只需要(等效于上面的写法):
// 等号左边的变量要和对象里面的属性同名,否则没法解构出来
// let {name, email, age} = obj; //{ }中的变量名和obj的属性名一致 完全解构
// 部分解构
// let {name} = obj; // 部分解构
//解构之后重命名
let {name:itsName} = obj; 解构之后重命名为itsName
//将现有对象的方法,赋值到某个变量
let {random}=Math;
console.log(random)//[Function: random]
数组解构
let arr1 = [10, 20, 30];
let [a, b, c , d] = arr1;
// 完全解构
console.log(a); //10
console.log(b); //20
console.log(c); //30
// 若解构不成功,变量的值就等于 undefined
console.log(d); //undefined
// 部分解构
let [e] = arr1;
console.log(e); //10
let [ , ,f] = arr1;
console.log(f); //30
// 复合解构
let arr2 = [1, 2, [10, 20, 30]];
let [ j, k, [x, y, z]] = arr2;
console.log(j); //1
console.log(k); //2
console.log(x); //10
console.log(y); //20
console.log(z); //30
字符串解构
let string1 = "xyz";
let [a,b,c] = string1;
console.log(a); //x
console.log(b); //y
console.log(c); //z
string1[1] = "Y";
console.log(string1); // xyz 无法修改
console.log(string1[1]); // y
应用—交换两个变量的值
let a = 10;
let b = 20;
[b,a] = [a,b]
变量 = 值;
console.log(a,b) // a=20,b=10
对象的简化写法
let name = '狗蛋',age = 12,gender = '男'
obj的属性名和变量是同样的,可以在es6中简化为:
let obj = {name,age,gender}
如果一个对象的属性名和外面的一个变量名同名,可以直接将变量名作为属性名,并会自动地把变量的值作为属性的值
函数参数默认值和参数解构
函数形参的默认值
ES5的方法:
function add(a,b,c,d){
a = a || 0;
b = b || 0;
c = c || 0;
d = d || 0;
return a + b + c + d;
}
ES6的方法:
funciton 函数名(参数=默认值){ // 注意当 参数 为 undefined 时 参数 赋值为 默认值
}
function add(a=0,b=0,c=0,d=0){
return a + b + c + d;
}
函数参数解构
// 参数是一组有次序的值
function f([x, y, z]) {
console.log(x, y, z);
}
f([1, 2, 3]);
// 参数是一组无次序的值
function fn({x, y, z}) { // {x, y, z} = obj 解构
console.log(x, y, z);
}
fn({z: 4, x: 5, y: 6});
解构赋值指定参数的默认值
function func2({name, age} = {}){ //防止不传实参时候的报错
console.log(name, age);
}
func2(); //undefined undefined
// func2(); //相当于传了一个null {name, age}=null 就会报错
// func2({}); //不会报错,输出:undefined undefined
function func2({name="luowowo", age=11} = {}){ //指定默认值
console.log(name, age);
}
func2(); //luowowo 11
rest 参数/剩余参数
ES5的方法:
arguments 对象:
function fn(){
console.log(arguments);// 伪数组
}
fn(10, 20, 30, 50, 60);
ES6的方法:
function func( a, b ,...rest){ // 把剩余的参数都交给rest
console.log(rest);
}
func(10, 20, 30, 50, 60);
function func2(...rest){ //rest 接收所有参数作为一个数组
rest.forEach(function (item) {
console.log(item);
});
}
func2(60, 70, 80, 90);
// 报错
function f(a, ...b, c) {
// ...
}
拓展运算符
// 快速将一个数组拆开成一个一个的元素
let arr = [1, 2, 3, 4]
console.log(...arr)
// 快速将一个对象里面的数据复制一份到一个新的对象里面
let obj = { name: '狗蛋', age: 12, gender: '男' }
console.log({id:1,birthday:'2020-02-02', ...obj})
// 将一个字符串拆开成为多个单独的字符
let str = 'abc'
console.log(...str)
// 1、数组中的值作为函数参数使用
let arr1 = [10, 20, 30];
function func(a, b, c){
console.log(a,b,c)
}
func(...arr1); //等效于:func(10,20,30); 输出结果10 20 30
// 2、合并数组
let arr2 = [40, 50, 60];
let newArr = [...arr1,...arr2]; // 等效于 [ 10, 20, 30, 40, 50, 60 ]
console.log(newArr); //[ 10, 20, 30, 40, 50, 60 ]
// 3、合并对象
let obj1 = {
name:"luowowo",
age:"11",
};
let obj2 = {
email:"luowowo@163.com",
};
let newObj = {...obj1,...obj2}; // 等效于{ name: 'luowowo', age: '11', email: 'luowowo@163.com' }
console.log(newObj); //{ name: 'luowowo', age: '11', email: 'luowowo@163.com' }
// 4、es6中另一个合并对象的方法
let newObj2 = Object.assign({},obj1,obj2); // 把第二个及第二个以上的参数都合并到第1个上面去。
console.log(newObj2); //{ name: 'luowowo', age: '11', email: 'luowowo@163.com' }
数组解构
let [a, b, c, ...arr] = [1, 2, 3, 4, 5, 6, 7];
console.log(a, b, c, arr); // a = 1, b = 2, c = 3, arr = [4,5,6,7]
解构字符串
let str = "abcde";
let [a, b, c, ...strArr] = str;
console.log(a, b, c, strArr); // a = 'a',b='b',c='c',strArr=['d','e']
箭头函数
基本语法
ES6 允许使用 “箭头”(=>)简化函数的定义。
(参数) => { 函数体 }
1. 形参个数如果为1个,可以省略小括号不写;
// 3、有一个参数 (函数只有一个函数,可以省略()不写
// let fn3 = (a) =>{console.log(a);}
let fn3 = a => console.log(a);
fn3(6)
2. 如果函数体里面只有一个语句,可以省略大括号不写, 并且他会默认返回 => 符号后面的数据。
// let fn1 =() =>{console.log("无参数无返回值");}
let fn1 = () => console.log("无参数无返回值");
fn1();
// 2、无参数有返回值 (函数体只有一句话(并且要返回),{}和return都可以不写
// let fn2 = ()=>{return "无参数有返回值"}
let fn2 = () => "无参数有返回值"
console.log(fn2());
3. 如果函数体有多个语句,则不能省略大括号。
4. 如果函数体只有一个语句,且返回一个对象,建议是,不要写简写的方式。
5. 箭头函数不可以使用 arguments 获取参数列表,可以使用 rest 参数代替。
let fn6 = (...a) =>{
console.log(a); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
}
fn6(1,2,3,4,5,6,7,8,9)
使用场景(回调函数)
// 定时器中的回调函数
setInterval(() => {
console.log("我用了箭头函数");
}, 1000);
// forEach中的回调函数
var arr = [22, 32, 11, 3, 5, 7, 88];
arr.forEach(item => console.log(item));
this指向
1. 全局使用(函数全局调用)指向window
// 1、全局使用——————指向windo(全局)
var name = "全局"
function fn(){
console.log(this);//Window
}
fn();
// 2、定时器中使用——————指向windo(全局)
setTimeout(function(){
console.log(this);//Window
},1000)
2. 对象调用指向该对象(事件中的事件源)
btn.onclick = function(){
console.log(this);
}
3. 箭头函数没有自己的作用域,即箭头函数 this 指向其外层作用域
改变指向——————对象.方法调用 和 箭头函数
Promise对象
功能:避免了回调地狱,把异步代码改成调用起来像同步代码。
// 语法:
// Promise————
// var p = new Promis(回调函数)
// 回调函数也有两个参数
// resolve ---------坚定
// reject-----------拒绝
var p = new Promise((resolve,reject)=>{
//默认状态——————pending
// 调用函数----resolve();
// 得到
// 成功状态————fulfilled
// 调用函数——--reject()
// 失败状态————rejected
// resolve();
reject();
})
console.log(p);
// 特点:1、转态不受外部影响
// 2、状态一旦发生改变,将不再变化
Promise的基本语法
// 默认pending: 初始状态
var p=new Promise(function (resolve,reject) {
if("操作成功"){
resolve();//pending-->fulfilled 异步操作成功的回调函数
}else{
reject(); //pending-->reject 异步操作失败的回调函数
}
})
p.then(data => {//在外面调用then处理成功的逻辑
console.log("处理成功的逻辑");//fulfilled
}).catch(err=>{//在外面调用catch处理失败的逻辑
console.log("失败的逻辑");//reject
})
// then方法会在异步成功后调用,catch方法会在异步失败后调用
如果需要连续调用,就要用return
比如:
p1.then(data1 => {
console.log(data1);
return p2
}).then(data2 => {
console.log(data2);
return p3
}).then(data3 => {
console.log(data3);
})
解决多重请求(回调地狱)
利用return 返回值
简化多重请求的promise写法
函数封装
function PromiseAjax(url){
return new Promise((resolve,reject)=>{
$.ajax({
url,
success(res){
resolve(res)
},
error(err){
reject(err)
}
})
})
}
let p1 = PromiseAjax('url1')
let p2 = PromiseAjax('url2')
let p2 = PromiseAjax('url2')
Promise的方法使用
all方法
- 只有
p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。 - 只要
p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("失败");
}, 3000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功2");
}, 2000);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功3");
}, 1000);
});
// 全部成功返回成功,有一个失败返回失败
let p = Promise.all([p1, p2, p3]).then((res) => console.log(res)).catch(err=>console.log(err));
race方法
只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
let p = Promise.race([p1, p2, p3])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log("网络状态不佳");
console.log(err);
});
异步代码同步化
async函数和await关键字一般成对出现,当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功1");
}, 3000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功2");
}, 2000);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功3");
}, 1000);
});
async function getVal(){
await p1.then(res=>console.log(res))
await p2.then(res=>console.log(res))
await p3.then(res=>console.log(res))
}
getVal()
对象
书写对象的语法:
{
key1:value1,
key2:value2
}
访问对象的属性:
console.log(person.key);//访问对象的属性
访问对象的方法:
person.eat();//访问对象的方法
对象内有两种成员
- name / age / sex等对象特征描述称之为对象的**属性**,就是我们之前学习的变量
- eat / sleep 等对象拥有的行为称之为对象的**方法**,也就是我们之前说的函数
对象的操作
- 访问
- person.name
person["age"]
- person.eat()
person["sleep"]()
- person.name
- 增加、修改
- person.name=“xxx”
- 已存在则修改,不存在则新增
- 删除
- delete person.name(不建议使用,避免删除了别人正在使用的对象)
- person.name = “”
数据安全
// 将 全局变量 全局作用域的函数 封装进对象里,这样就只有一个全局变量了,就是我们创建的对象obj
// 创建一个对象,更多的是一种解决方案,一种编程思想
面向过程和面向对象编程
- 面向过程编程就是分析出解决问题的步骤,然后使用函数把这些步骤一步步实现,重心放在完成的每个过程上。
- 面向对象则是以封装的思想,重点放在解决问题需要的对象身上,然后通过对对象的操作来完成相应的功能。
两者比较
- 面向过程性能比面向对象高,适合跟硬件联系很紧密的东西,但是不易维护、不易复用、不易扩展。
- 面向对象易维护、易复用、易扩展,但是更耗资源,性能比面向过程低。
- 至于以后使用哪一种,这就需要看我们的具体需求,根据不同的需求做不同的选择。
创建对象
字面量方式创建对象
直接使用字面量方式创建对象比较方便,以键值对的格式来定义数据
例如:
key:value
var book1 = {
name:"JavaScript权威指南",
price:100,
author:"作者",
showInfo:function () {
console.log("描述信息");
}
}
console.log(book1);
优点:方便直观,可以直接访问里面的属性方法;
缺点:创建大量相或相似对象时,会出现代码重复,只适合创建单个对象
内置构造函数创建对象
使用new关键字+内置的构造函数创建对象
var book = new Object();
book2.name="JS";
book2.price=10;
book2.author="作者";
book2.showInfo=function () {
console.log("描述信息");
}
book2.showInfo();
缺点:和字面量创建存在一样问题:代码重复
简单工厂创建对象
函数封装思想,将这些重复代码抽取到函数中来解决。
function createBook(name, price, author) {
var book = new Object();
book.name=name;
book.price=price;
book.author=author;
book.showInfo=function () {
console.log(this.name,this.price,this.author);
}
return book;
}
var book3 = createBook("bookName1",10,"author1");
var book4 = createBook("bookName2",10,"author2");
console.log(book3);
console.log(book4);
优点:值是活的,可以批量操作,减少重复代码
缺点:无法判断对象类型
自定义构造函数创建对象
语法:
function 函数名(参数列表){
this.key1=参数1,
this.key2=参数2,
}
var obj = new 函数()
举例:
function CreatePerson(name, age, sex) {
this.name=name;
this.age=age;
this.sex=sex;
}
var p = new createPerson("Neld", 10, 1);
var p2 = new createPerson("Song", 12, 0);
console.log(p);
console.log(p2);
自定义构造函数和工厂函数的区别
- 构造函数名的首字母要求大写
- 在函数中,不需要手动创建对象进行数据封装,会自动创建对象并封装数据
- 在函数最后,不需要手动返回创建好的对象,会自动返回
- 构造函数一样可以直接调用,此时内部的this执行window,这种方式不太安全,有可能会在函数内部修改当前的全局变量,不建议使用,而且这样做也不能创建对象,必须要搭配new关键字一起使用才能创建对象
自定义构造函数到底是如何创建对象的?
也就是new 这个关键字做了什么事情(实例化):
- 在函数内部默认会创建一个空对象——var obj = new Object();
- 默认把创建好的对象赋值给this——this = obj;
- 通过this添加属性和方法——this.xx=xx……
- 默认会把内部创建的对象返回——return this;
抽象
- 构造器(类):
- 泛指一类事物
- 把多个对象相同的部分抽象出来,成为一个类,就是一个函数,构造函数,和new一起来创建对象,
- 对象:
- 特质某一个具体事物
- 使用构造函数创造出来的对象的类型就是构造函数这种类型
分类
constructor属性
定义:使用constructor属性可以获取到创建对象使用的构造器函数(类)。
语法:对象.constructor——————获取到就是该对象的类
function Person(name) {
this.name = name;
}
function Dog(name) {
this.name = name;
}
var p = new Person("p");
var d = new Dog("d");
console.log(p.constructor);//打印得到Person函数对象
console.log(d.constructor);//打印得到Dog函数对象
if(p.constructor === Person){
console.log("p是Person对象");
}
if(d.constructor === Dog){
console.log("d是Dog对象");
}
instanceof关键字
定义:instanceof关键字用来判断对象的类型是否是某个类,如果是返回true,反之返回false。
语法:var ret = 对象名 instanceof 类名;————————获取到的是一个布尔值
function Person(name) {
this.name = name;
}
function Dog(name) {
this.name = name;
}
var p = new Person("p");
var d = new Dog("d");
console.log(p instanceof Person);//true
console.log(d instanceof Person);//false
this指针
this总是指向一个对象(引用类型),但是具体指向谁,需要根据我们在哪里使用this有关。这里主要分为下面几种情况:
-
全局使用
函数外部使用,作用域即使是全局作用域(window),所以,在全局作用域中使用的this指向window
this在函数内部,全局调用这个函数,也是在全局使用,此时函数内的this也指向window
-
对象方法的调用 例如:obj.fn()
函数内部的作用域是局部的,属于调用当前函数的对象,所以this执向调用当前函数的对象
包括事件绑定,也是一种对象.方法()的使用
-
箭头函数中使用
箭头函数没有作用域,箭头函数中的this指向外层作用域
-
构造函数内部
在构造函数中,this直接执行当前创建出来的新对象
原型对象
语法:
构造函数.prototype
获取原型对象构造函数.prototype.成员名 = 成员值
在原型对象上添加成员的方法
获取原型对象的方法
__ proto __属性
定义:在每个实例对象上都有一个__ proto __的属性,也是用来获取该对象的原型对象(该属性是在ES6之后才纳入规范,在这之前,只有部分浏览器实现。)。
语法:
`实例对象.__ proto __;`
实例对象.__ proto __ === 构造器.prototype;
getPrototypeOf方法
定义:Object构造器上的静态成员方法。
语法:Object.getPrototypeOf(实例对象)
获取指定实例对象的原型对象
以上提到的三种获取原型对象的方法所得到的结果是一样的。即:
Object.getPrototypeOf(p) == Person.prototype == p.__ proto __
//代码不能这么写,要分开比较
构造函数创建实例对象的完整过程:
1. 在函数内部默认会创建一个空对象——var obj = new Object();
2. 设置`obj.__proto__` 属性指向构造器.prototype——`obj.__proto__ = Person.prototype`;
3. 默认把创建好的对象赋值给this——this = obj;
4. 通过this添加属性和方法——this.xx=xx……
5. 默认会把内部创建的对象返回——return this;
原型对象的设置和访问
在原型对象上添加单个成员
//构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
//在原型对象中添加方法
Person.prototype.study = function () {
console.log(this.name,this.age);
}
//创建实例对象
var p = new Person("zs",10);
var p2 = new Person("ls",12);
p.study();
p2.study();
在原型对象上添加多个成员
Person.prototype = {
study:function () {
console.log(this.name,this.age);
},
say:function () {
console.log(this.name," say hello");
}
}
注意点:**上面这段代码必须写在创建实例对象之前,否则添加的成员不可用。**
原型对象的访问规则
实例对象没有constructor属性,其实是通过__proto__
共享了其原型对象上的属性
// p.constructor指向 原型链 __proto__链接的这条线
// 原型链访问规则:顺着__proto__网上去找,爸爸身上找不到,去爷爷身上找
// 1、整那个原型链都找不到,得到undefined
// 2、就近原则(爸爸身上有,就不去爷爷身上找,自己身上有,哪都不去找)
原型相关属性学习
in关键字
定义:用来检查对象中是否存在某个指定的属性(不区分实例属性和原型属性),
语法:"属性名" in 实例对象
用法:无论判断的成员是属于当前实例对象还是属于其原型对象的,都返回true,如果都不存在,则返回false。
// 1、方法一:in 判断是不是实例成员和原型成员
for(var k in p){
console.log(k in p);//true
}
console.log("name" in p);//true
console.log("eat" in p);//true
hasOwnProperty方法
定义:Object的原型成员,所有实例对象都能访问的方法,
语法:实例对象.hasOwnProperty("属性名")
用法:只判断当前实例对象中是否存在实例的属性,存在返回true,反之返回false。
// 2、方法二: hasOwnProperty()判断属性是不是只属于对象的实例成员
console.log(p.hasOwnProperty("name"));//true
console.log(!p.hasOwnProperty("name") && "name" in p);//false
console.log(p.hasOwnProperty("name") && "name" in p);//true
isPrototypeOf方法
定义:Object的原型成员,判断某个对象是否是指定对象的原型对象
语法:原型对象.isPrototypeOf(实例对象)
用法:A.isPrototypeOf(B) 判断的是A对象是否存在于B对象的原型链之中
// 3、方法三 某个原型对象.isPrototypeOf(实例对象)
// 判断原型对象在不在 实例对象的原型链上
console.log("-----------------------------");
console.log(Person.prototype.isPrototypeOf(p));//true
console.log(Object.prototype.isPrototypeOf(p));//true
console.log(Array.prototype.isPrototypeOf(p));//false
instanceof关键字
定义:字面意思理解为判断当前对象是否是指定的类型,更深层次理解应该是,指定类型是否在当前实例对象的原型链上,如果是返回true,反之返回false。
语法:实例对象 instanceof 构造函数
用法:A instanceof B 判断的是B.prototype是否存在与A的原型链之中
// 4、方法四 A instanceof B A(实例对象)是不是类型B(构造函数)
// 判断的是B 的构造函数的原型对象,在不在 A (实例对象)的原型链上
console.log(p instanceof Person);//true
console.log(p instanceof Object);//true
console.log(p instanceof Array);//false
面向对象三大特性
1、封装
定义:使用对象封装一些变量和函数
作用:复用和信息隐藏
详解:封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
2、继承
定义:一个类获取另外一个类属性和方法的一种方式
作用:代码复用
详解:继承,就是可以使用已创建好的类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
- 通过继承创建的新类称为“子类”或“派生类”。
- 被继承的类称为“基类”、“父类”或“超类”。
- 继承的过程,就是从一般到特殊的过程。
3、多态
定义:同一个操作,作用于不用的对象,会有不同的行为
作用:具有可拓展性
详解:多态首先是建立在继承的基础上的,先有继承才能有多态。多态是指不同的子类在继承父类后分别都重写覆盖了父类的方法,即父类同一个方法,在继承的子类中表现出不同的形式。js天生就具备多态的特性(弱类型语言)
继承
ES5是没有继承语法的,只能通过方法模拟继承语法,所以有多种方式达到继承效果。
继承目的:通过相应的代码将父类中的成员复制到子类对象中
混入式继承
实现原理:将父类成员拷贝到子对象中(浅拷贝)。
实现方法:for…in…循环遍历父类,子类[key]=父类[key]
缺点:共享数据安全问题,修改子类,会影响父类,引用数据类型浅拷贝,会修改引用地址
//混入式继承(拷贝继承)
//obj2继承到obj1中的成员,可以直接将obj1中的成员拷贝到obj2中即可
var obj1 = {name:"zs",age:10};
var obj2 = {};
// 将obj1中的成员拷贝到obj2中
for (var key in obj1) {
obj2[key] = obj1[key];
}
console.log(obj1);
console.log(obj2);
原型式继承
实现原理:将父类中的原型成员添加到子类的原型链中。
实现方式:子类.prototype = 父类.prototype
缺点:数据共享安全,只能继承父类原型对象中成员,不能继父类实例对象成员
function Animal() {
}
Animal.prototype.name="animal";
function Person() {
}
//修改Person的原型对象
Person.prototype= Animal.prototype;
Person.prototype.useTool = function () {
console.log("use fire");
}
var p = new Person();
console.log(p);
var ani = new Animal();
console.log(ani);
原型链继承
实现原理:将子类的原型对象指向父类的实例对象。
实现方法:子类.prototype = new 父类()
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.getInfo = function () {
console.log("name:",this.name,"age:",this.age);
}
function Student(score) {
this.score = score;
}
Student.prototype = new Person("小明",10);//继承父构造函数并设置name和age的值
Student.prototype.getScore = function () {
console.log("分数:"+this.score);
}
var p1 = new Student(100);
p1.name="xxx"//只能这样一个个修改,无法向父类传参
var p2 = new Student(12);
console.log(p1);
console.log(p2);
call方法和apply方法的基本使用
定义:将方法借给某个对象的方法。call和apply作用相同,写法不同。
使用call方法的语法:
被借用对象.方法.call(借用对象)
使用apply方法的语法:
被借用对象.方法.apply(借用对象)
call和apply的区别(传参方式不同):
// call传参,跟在借用对象后面,用逗号隔开
被借用对象.方法.call(借用对象,参数1,参数2……)
// apply传参,不能直接写在后面,要将参数封装在数组中跟在借用对象后面,用逗号隔开
被借用对象.方法.apply(借用对象,[ 参数1,参数2…… ])
借用构造函数继承
实现原理:在子构造函数中调用父构造函数,达到继承并向父构造函数传参的目的。
实现方法:
- 将父对象的构造函数设置为子对象的成员
- 调用这个方法,类似于将父构造函数中的代码复制到子构造函数中来执行
- 用完之后删掉
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.getInfo = function () {
console.log("name:",this.name,"age:",this.age);
}
function Student(name,age,score) {
this.newMethod = Person;//1.将父对象的构造函数设置为子对象的成员
this.newMethod(name, age);//2.调用这个方法,类似于将父构造函数中的代码复制到子构造函数中来执行
this.score = score;
delete this.newMethod;//3.用完之后删掉
}
Student.prototype.getScore = function () {
console.log("分数:"+this.score);
}
var p1 = new Student("Neld", 10 ,100);
var p2 = new Student("Lily", 12 ,80);
console.log(p1);
console.log(p2);
优化:高级实现方法:凡是要借用方法,首先想到使用call或apply
function Student(name,age,score) {
//Person.call(this,name,age);
Person.apply(this,[name,age]);
this.score = score;
}
这种继承方式都存在下面两个问题:
- 如果父子构造函数存在相同的成员,那么子构造函数会覆盖父构造函数中的成员
- 不能继承原型链中的成员
组合继承
实现原理:基原型链继承+借用构造函数继承。
function Student(name,age,score) {
//Person.call(this,name,age);
Person.apply(this,[name,age]);//继承构造函数中的成员
this.score = score;
}
Student.prototype = new Person();//继承原型链上的成员
Student.prototype.constructor = Student
缺点:子类的原型对象上有无用属性
寄生组合继承
实现原理:组合继承的基础上,改造原本的原型链继承。子类和父类中间创建一个空类,过滤掉无用的父类实例属性。
// 寄生式组合继承,在父子类中间再加一层,去掉子类原型对象上的无用属性
(function () {// 4.创造一个独立的作用域,用完失效
// 1.创建一个没有实例成员的Super类
var Super = function () { };
// 2.将Super类的原型对象指向父类的原型对象
Super.prototype = Person.prototype;
// 3.将Super类的实例作为子类的原型
Student.prototype = new Super();
Student.prototype.constructor = Student;
})();
总结: ES5实现继承的方式不止一种。这是因为ES5 中的继承机制并不是明确规定的,而是通过模仿实现的。这意味着所有的继承细节并非完全由解释程序处理。作为开发者,你有权决定最适用的继承方式。
基本包装类型
基本包装类型的创建
定义:方便对string,number,boolean三种基本类型数据操作,ECMAScript提供了三个特殊引用类型——基本包装类型(String,Number,Boolean)
创建方式;
// 方式一:
var str1 = new String("string1");
var num1 = new Number(123);
var bool1 = new Boolean(true);
console.log(str1);
console.log(num1);
console.log(bool1);
// 方式二
var str2 = new Object("string2");
var num2 = new Object(456);
var bool2 = new Object(false);
console.log(str2);
console.log(num2);
console.log(bool2);
str是基本类型,不存在属性和方法一说,但是下面两行代码中却是在访问属性和方法,而且代码还能正确的执行,这是如何实现的呢?
其实,为了让我们实现这种直观的操作,后台已经自动完成了以下操作:
- 创建 String 类型的一个实例;
- 在实例上调用指定的方法;
- 销毁这个实例;
私有成员和特权方法
成员:对象的属性和方法
实例成员:实例对象的属性和方法 name\age | showName\showClass
静态成员:构造函数自己的属性和方法 info
原型成员:构造函数对应的原型对象的属性和方法 des
私有成员:在构造函数中声明的变量和函数,因为我们只能在函数内部访问,所以是私有的 className getClassName
特权方法:在函数内部使用了私有成员的实例方法被称为是特权方法(闭包) showClass
Object成员
原型成员
-
constructor:获取当前对象的构造函数
-
hasOwnProperty:判断当前实例对象中是否存在指定的属性
-
isPrototypeOf:判断当前对象是否在指定对象的原型链中
-
propertyIsEnumerable:实例成员是否可以枚举(循环遍历)返回布尔值
-
valueOf:返回当前对象对应的值。
- 三大包装类型(还有Date)都有原型成员valueOf,所以基本类型使用valueOf是使用自己包装类型的valueOf原型方法
- Object.prototype.valueOf.call(…);才能真正使用Object的valueOf原型方法
- 特点:基本类型会得到包装类型的返回值
var strObj = new String("demo");
console.log(strObj.valueOf());//demo 基本包装类型:返回对应的值
var obj = {name:'ls',age:18};
console.log(obj.valueOf());//{name:'ls',age:18} 返回对象本身
var date = new Date();
console.log(date.valueOf());//1559489503989 返回时间戳
Object.prototype.valueOf.call(1);
Object.prototype.valueOf.call("");
Object.prototype.valueOf.call(true);
toString:返回数据特定的格式的字符串 [object 构造函数]。
- 几乎所有的构造函数都有原型成员toString,所以字符串,数字,布尔,数组等类型使用toString是使用自己构造函数的toString原型方法
- Object.prototype.toString.call(…);才能真正使用Object的toString原型方法
- 作用:可以获取所有数据的真实类型
// 其实调用的是包装类型的toString方法
console.log("abc".toString());//"abc"
console.log((123).toString());//"123"
console.log((100).toString(2));//1100100
console.log((100).toString(16));//64
console.log(true.toString());//"true"
// 对象使用的才是Object的toString原型方法
function Person(name) {
this.name = name;
}
var p = new Person("Neld");
// 第一个object为对象的类型,第二个Object为对象对应的构造函数
console.log(p.toString());//"[object Object]"
// 数组会调用Array的toString方法
var arr = [1,2,"A",false];
console.log(arr.toString());//"1,2,A,false"
静态成员
assign:将多个对象合并到一个对象中并返回
var obj = {name:"Neld", age:10};
console.log(Object.assign(obj, {info: "xxx"}, {name: "zs"}));
// 返回结果为:{name:"zs", age:10, info:"xxx"},如果多个对象想存在相同的属性,后面会将前面属性值覆盖。
create:创建对象,并设置原型对象
// 该方法可以接收的参数有一下两种
// 1. null 创建一个空对象,这个空对象中连最基本的原型对象都没有的
Object.create(null)//创建一个空对象
// 2. 对象 创建传递进来的对象,并设置该对象的原型对象为当前的参数
Object.create({……})//创建出的对象,给原型对象添加……成员
is:判断两个参数是否相等,等同于===,注意下面两种特殊的判断即可
console.log(0 === -0); //true
console.log(Object.is(-0, 0)); //false
console.log(NaN === NaN); //false
console.log(Object.is(NaN, NaN)); //true
values:获取当前对象所有属性的值,合并成一个数组并返回
var obj={name:"小明",age:10,gender:1};
Object.values(obj) //["小明", 10, 1]
ES6类的实现——class
class的基本结构
定义及用法:calss关键字定义类,创建构造函数,类名首字母大写
语法结构:
class 类名{
constructor(参数1,参数2){
// 构造函数体,添加实例对象成员
}
方法名(){
// 添加原型对象成员
}
static 方法名{
// 添加静态成员,只能用类调用
}
}
var d=new 类名
ES5中,使用function定义一个类,并使用它来创建对象。
function Person(name, age){
//实例成员
this.name = name;
this.age = age;
}
Person.prototype.doWork = function(){
console.log("ES5中在原型对象上添加方法");
}
var p = new Person("Neld", 10);
在ES6中,使用class关键字定义同样的类。
class Person{
constructor(name,age){
this.name = name;
this.age = age;
}
doWork(){
console.log("ES6中在原型对象上添加方法");
}
}
var p = new Person("Neld",10);
console.log(p);
ES6中使用class定义类只是一种语法糖(语法糖能够增加程序的可读性,从而减少程序代码出错的机会的一种语法),底层最终还是转换成ES5中使用的function类定义类,以及其中的实例成员和原型成员。
class使用的细节:
- constructor方法是创建对象的构造方法,通常在这里为实例对象定义属性(方法也是可以的),new 之后会自动调用。
- constructor方法外面的方法是对象的原型方法。
- 在之前外面还为构造方法添加过成员(静态成员),前面要加static。
class Person{
constructor(name,age){
this.name = name;
this.age = age;
}
static doWork(){
console.log("ES6中在原型对象上添加方法");
}
}
Person.doWork()
class的继承结构
ES6中class用extends 和 super实现继承。
语法结构:
class 子类 extends 父类{
constructor(参数1,参数2){
//调用父类构造函数,将数据封装到对应属性中
super(参数1,参数2);
}
}
class实现继承的细节:
-
Animal中定义了动物都应该有的属性和方法
-
使用extends关键字实现Person类继承Animal类的功能,此时他们两就属于继承关系了。
-
在Person的构造方法中,使用super关键字调用父类中的构造方法。
面试题
立即执行函数
定义:声明一个函数,并马上调用这个匿名函数就叫做立即执行函数或者即时函数(匿名函数自执行)Immediately-Invoked Function Expression (IIFE)
特点:
- 首先是个匿名函数,无需函数命名。
- 主要作用是:防止全局变量污染,便于多人协作开发。
- 函数需要立即执行,且只需要执行一次,执行一次之后不再调用,通常用于需要初始化的变量。
·语法结构:
// 掌握这一种
(function (形参) {
console.log("方式一");
})(实参);
立即执行函数的作用:
- 避免了污染全局变量
- 封装变量
- 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
点击事件-----用立即执行函数解决
给每个 li 创造一个独立作用域,立即执行函数执行的时候,i 的值被赋值给 ii,此后 ii 的值一直不变,实现了全局变量私有化。
var liList=document.getElementsByTagName('li');
for(var i=0;i<liList.length;i++){
(function(ii) {
liList[ii].onclick=function(){
console.log(ii);
}
})(i)
};
闭包
function fun() {
var a = 123;
function fun2() {
return a;
}
return fun2;
}
var fun3 = fun();
console.log(fun3());//123
在函数fun中定义了另一个函数fun2,fun2中访问函数外部的局部变量a,最后将fun2返回。
调用fun得到返回值(fun2函数),因为该返回值是一个函数,所以我们调用得到函数中返回的a的值。
定义:像这种函数中返回另一个函数的结构称为闭包。
特点:
- 打破作用域的限制,让外部访问函数内部变量成为可能,延长变量的生命周期;
- 可以避免使用全局变量,防止全局变量污染;
- 保护私有变量不会被随意修改;
- 局部变量会常驻在内存中,会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
定时器时间和闭包的执行
需求:使用setTimeout开启10个定时器,分别输出 0,1,2,3,4,5,6,7,8,9。
直接使用for循环---------for循环会一瞬间完成,定时器是异步,此时已经全部是0。
闭包解决
for (var i = 0; i < 10; i++) {
function fun() {
return function (j) {
console.log(j);
};
}
//如果setTimeout内函数需要参数
//实参是在setTimeout的第二个参数(时间)之后。
setTimeout(fun(), 1000 *i, i);
}
DOM事件和闭包的
延长事件返回值的生命周期
<ul id=”test”>
<li>这是第一条</li>
<li>这是第二条</li>
<li>这是第三条</li>
</ul>
<script>
var liList=document.getElementsByTagName('li');
//立即执行函数
for (var i = 0; i < liList.length; i++) {
(function fun(j) {
liList[j].onclick=function () {
console.log('第'+ (j+1) +'个li');
}
})(i)
}
// 闭包
for (var i = 0; i < div.length; i++) {
div[i].onclick=(function (j) {
return function(){
console.log('第'+ (j+1) +'个div');
}
})(i)
}
</script>
递归
递归的基本结构
定义:函数中用调用函数自己的结构称作递归
function f1() {
console.log("从前有座山,山里有个庙,庙里有个老和尚给小和尚讲故事:");
f1();
};
f1();//浏览器崩溃,因为没有结束条件——死循环
递归两个要素
1.递归的边界——找到出口,在什么情况下跳出递归
2.递归的逻辑——找到入口,什么情况下重复调用自己,调用自己做什么
var i=0;
function f1() {
i++;
if (i<5){// <5的时候就是入口
f1();
} // =5的时候就是出口
console.log("从前有座山,山里有个庙,庙里有个老和尚给小和尚讲故事:");
};
f1();
递归阶乘
需求:封装一个方法,计算正整数num的阶乘(递归阶乘)
什么是阶乘(factorial):所有小于及等于该数的正整数的积
// 递归阶乘
function factorial(num) {
if(num <= 1) { return 1 };
return num * factorial(num - 1);
}
拷贝
浅拷贝实现
需求:就是将p1对象中的属性或者是方法拷贝到p2对象中。
我们首先想到是的for…in循环,将p1的属性拷贝一份到p2中:
缺点:对象内属性是引用数据类型的话,拷贝过来的就是引用地址,并没有实现真正的拷贝,依然同一份数据。
深拷贝实现
所以要实现深拷贝,当我们发现属性对应的值是一个对象或数组的时候,应该将该对象或数组再拷贝一份,然后赋值给当前属性。
思路:
- 浅拷贝拷贝基本数据类型
- 判断是否引用数据类型
- 递归调用,完成所有层次拷贝
- 判断value是数组还是对象
var p={
name:'小明',
age:13,
favs:['H5','Java','C'],
wife:{
name:'小丽',
age:15,
favs:['H5','Java','C']
}
};
var p2={};
function deepCopy(source,target) {
for (var Key in source) {
//只拷贝当前对象的属性
if(source.hasOwnProperty(Key)) {
//选择引用数据类型
if (typeof source[Key] == 'object') {
//判断value是数组还是对象
target[Key] = Array.isArray(source[Key]) ? [] : {};
//递归,深层次拷贝
deepCopy(source[Key], target[Key])
//arguments.callee(source[Key],target[Key])
}else{
target[Key] = source[Key]
}
}
}
}
deepCopy(p,p2)
console.log(p2);
通过上面的深度拷贝得到的p2对象是和p1完全不同的两份数据,此时不再存在数据共享的问题。
排序
冒泡排序法(Bubble Sort)
一种计算机科学领域的较简单的排序算法。
基本思路是:重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来
var arr = [5,3,9,4,6];
// 将数组中的数两两比对,调换顺序
// 轮数
for(var i = 0;i<arr.length-1;i++){
// 次数
for(var j = i+1;j<arr.length;j++){
// var temp;
if(arr[i]<arr[j]){
/*temp = arr[i];
arr[i]=arr[j];
arr[j]=temp;*/
[arr[i],arr[j]]=[arr[j],arr[i]];
}
}
}
console.log(arr)
快速排序法(Quick Sort)
快速排序是对冒泡排序的一种改进。
基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
需求:随机获取10到999之间的100个整数,并且要从大到小排序,要求使用快速排序算法。
//获取到min到max-1的total个随机数
function getRandomNum(total,min,max){
let arr=[];
for(let i = 0;i<total;i++){
arr.push(Math.floor(Math.random()*(max-min)+min))
}
return arr
}
//快速排序算法
function arrSort(arr) {
if (arr.length <= 1) { return arr; }
var index = Math.floor(arr.length / 2);
var middle = arr.splice(index, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++) {
if (arr[i] < middle) {
right.push(arr[i])
} else {
left.push(arr[i])
}
}
return arrSort(left).concat(middle, arrSort(right))
}
console.log(arrSort(getRandomNum(100,10,1000)))
设计模式
单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
核心:确保只有一个实例,并提供全局访问。
实现:假设要设置一个管理员,多次调用也仅设置一次,我们可以缓存一个内部变量来实现这个单例。
var _instance;
function Person() {
if(_instance){
console.log('之前已经创建过,直接返回之前创建的对象');
return _instance;
}
_instance = this;
console.log("创建了对象");//第一次new会打印这句话
}
var p1 = new Person();
var p2 = new Person();
console.log(p1 === p2); // true,他俩是同一个对象
观察者模式(Observer pattern)
定义:定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
举例:
- 售楼部的客服小姐姐—有新的房源—被观察者Subject
- 直接通知给不同的潜在客户—观察者Observer
- 不同客户做出不同的反应
// 1.被观察者类——售楼小姐姐
class Subject {
// 2.创建构造函数
constructor() {
// 3.给被观察者一个状态
this.state = 0; // 状态
this.observer = []; //观察者集合
}
// 4.设置状态的方法
setState(val) {
this.state = val;
// 11.什么时候状态更新了
this.stateChange();
}
// 5.获取状态的方法
getState() {
return this.state;
}
// 7.被观察者也要接受观察者信息
attach(observer) {
this.observer.push(observer);
}
// 10.小姐姐状态更新,客户做什么
stateChange() {
this.observer.forEach(function (item) {
item.update();
});
}
}
// 1.观察者类——客户
class Observer {
// 2.创建构造函数
constructor(name, subject) {
this.name = name;
// 6.观察者观察小姐姐
this.subject = subject;
// 8.将自己传递给被观察者
this.subject.attach(this);
}
// 9.观察者看到状态变化需要做什么
update() {
console.log(
`${this.name} 看到更新了 ,我的状态是:${this.subject.getState()}`
);
}
}
// 12.创建对象
var s = new Subject();
var o1 = new Observer("欧阳三", s);
var o2 = new Observer("南宫四", s);
var o3 = new Observer("西门五", s);
s.setState(1);
s.setState(4);
s.setState(7);
发布-订阅模式(Publish–subscribe pattern)
定义:类似观察者模式,只有中间多了一个调度中心。
易混淆点 :很多资料都会将观察者模式和发布订阅模式混为一谈,因为都是存在两个角色实现一对多关系(被观察者-观察者,发布者-订阅者),确实容易混淆,他们忽略了发布订阅模式里的经纪人角色或者叫调度中心。
原生AJAX(xhr)
JS的Ajax对象:XMLHttpRequest 对象用于在后台与服务器交换数据。
响应处理和响应流程
响应处理,即对服务响应回浏览器的数据根据状态码和 AJAX 对象的状态信息进行不同的处理,在绑定状态改变的处理函数中写对应的逻辑代码即可。
AJAX 对象中有 4 个属性:
- readyState 总共有 5 个状态值,分别为 0 ~ 4,每个值代表了不同的含义:
- 0:初始化,AJAX 对象还没有完成初始化
- 1:载入,AJAX 对象开始发送请求
- 2:载入完成,AJAX 对象的请求发送完成
- 3:解析,AJAX 对象开始读取服务器的响应
- 4:完成,AJAX 对象读取服务器响应结束
- status 表示响应的 HTTP 状态码,常见状态码如下:
- 200 OK:请求成功
- 302 Found:重定向,新的 URL 会在 response 中的 Location 中返回,浏览器将会使用新的 URL 发出新的 Request
- 304 Not Modified:已缓存,文档已经被缓存,直接从缓存调用
- 400 Bad Request:客户端请求有语法错误,不能被服务器所理解
- 403 Forbidden:服务器收到但拒绝服务,引用外部资源触发防盗链
- 404 Not Found:找不到资源,请求资源不存在
- 500 Internal Server Error:服务端错误,服务器发生了不可预期的错误
- 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常
- responseText 获得字符串形式的响应数据。
- responseXML 获得 XML 形式的响应数据。
综合以上,在状态改变的处理函数一般针对 readyState == 4 且 status == 200 的情况才处理,再根据后台返回的数据类型决定从 responseText 或者 responseXML 获取服务器响应回去来的数据。
使用ajax发送get请求
步骤:
// 1、创建 AJAX 对象;new XMLHttpRequest()
// 2、设置请求路径,请求方式等;ajax.open(请求方式,路径)
// 3、绑定监听状态改变的处理函数,在处理函数可获取响应数据;ajax.onreadystatechange
// 4、发送请求。ajax.send
<body>
<button id="btn">发送get请求</button>
<script>
btn.onclick = function(){
// 1、创建 AJAX 对象;
var ajax = new XMLHttpRequest();
// 2、设置请求路径,请求方式等;ajax.open(请求方式,路径)
ajax.open('get', 'http://kumanxuan1.f3322.net:8809/travels/query');
// 3、绑定监听状态改变的处理函数,在处理函数可获取响应数据;
ajax.onreadystatechange = function(){
// console.log(ajax.readyState);
if(ajax.readyState==4 && ajax.status==200){
// 此时获取服务器发过来的数据
console.log(ajax.responseText) // 得到的是字符串对象,需要用JSON.parse(txt)转对象
}
}
// 4、发送请求。
ajax.send();
}
</script>
</body>
使用ajax发送post请求
post请求需要传递参数给后台,
常见的Content-Type:
- application/x-www-form-urlencoded 浏览器默认
- application/json 来告诉服务端消息主体是序列化后的 JSON 字符串
- multipart/form-data 表单上传文件时
<body>
用户名:<input id="user" type="text"><br>
密码:<input id="pwd" type="password"><br>
<button id="btn">登录</button>
<script>
// 后台要求携带参数:
// 用户名 - username
// 密码 - password
btn.onclick = function () {
// 1、创建 AJAX 对象
var ajax = new XMLHttpRequest();
// 2、设置请求路径,请求方式等
ajax.open("post", "http://kumanxuan1.f3322.net:8809/users/login");
// 3、设置请求头,不然无法传递参数到后台
ajax.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
// 4、绑定监听状态改变的处理函数
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
console.log(ajax.responseText);
}
}
// 5、发送请求并携带参数
ajax.send(`username=${user.value}&password=${pwd.value}`)
}
</script>
</body>
JSON数据处理
eval函数的基本使用
作用:将字符串类型的参数转换成JS代码。
eval函数Function的区别:
-
Function:需要调用函数,才会执行代码
-
eval:转换之后立即执行
在开发中的应用场景:
当获取到一个字符串类型的数据,但是想将其转换成JS代码执行的时候使用该函数可以实现
var jsonStr = "({name:'Neld',age:10})";
console.log(eval(jsonStr));//转换成JS的对象
在实际开发中不建议使用eval函数,原因:
- 存在安全隐患
- 影响程序的执行性能
- 事实上,目前只有后端在用eval这种方式,前端开发有更好的方式:JSON方法。
JSON数据格式
定义:JSON 格式(JavaScript Object Notation 的缩写)是一种用于数据交换的文本格式。
JSON 对值的类型和格式有严格的规定:
- 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
- 原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和
null
(不能使用NaN
,Infinity
,-Infinity
和undefined
)。 - 字符串必须使用双引号表示,不能使用单引号。
- 对象的键名必须放在双引号里面。
- 数组或对象最后一个成员的后面,不能加逗号。
- 不能注释
// 以下都是合法的 JSON:
["one", "two", "three"]
{ "one": 1, "two": 2, "three": 3 }
{"names": ["张三", "李四"] }
[ { "name": "张三"}, {"name": "李四"} ]
// 以下都是不合法的 JSON:
{ name: "张三", 'age': 32 } // 属性名必须使用双引号
[32, 64, 128, 0xFFF] // 不能使用十六进制值
{ "name": "张三", "age": undefined } // 不能使用 undefined
{ "name": "张三",
"birthday": new Date('Fri, 26 Aug 2011 07:13:10 GMT'),
"getName": function () {
return this.name;
}
} // 属性值不能使用函数和日期对象
JSON对象是 JavaScript 的原生对象,用来处理 JSON 格式数据。它有两个静态方法:
JSON.stringify(JS对象);//序列化:JS对象转JSON字符串
JSON.parse('JSON字符串');//反序列化:JSON字符串转成相应的数据格式(JS对象)
需求:使用JSON方法实现深拷贝。
var data = [
{ name: "小龙", age: 40 },
{ name: "老陈", age: 35 }
];
var jsonData=JSON.stringify(data);//JS对象转JSON字符串
console.log(jsonData);
var newData=JSON.parse(jsonData);//JSON字符串转成相应的数据格式(JS对象)
// 此时已经实现了深拷贝,它虽然很香,但是面试题不会考你用这种方式实现深拷贝
console.log(newData);
console.log('---------------------------------');
newData[0].age=50;
console.log(data);
console.log(newData);
正则表达式
正则的作用就是:判断字符串是否满足一定的规则
正则的创建
// 构造函数
let reg = new RegExp('规则','匹配模式')
// 字面量
let reg = /pattern/flags;
/*
pattern:正则表达式
flags:标识(修饰符)
标识主要包括:
1. i 忽略大小写匹配
2. m 多行匹配,即在到达一行文本末尾时还会继续寻常下一行中是否与正则匹配的项
3. g 全局匹配 模式应用于所有字符串,而非在找到第一个匹配项时停止
*/
// 匹配字符串里面有没有a
let reg = new RegExp('a')
// 或者
let reg = /a/
// 调用 test 方法对字符串进行验证
reg.test('123') // false
reg.test('abc') // true
正则表达式的规则
特定规则
\d 匹配数字 \D 和\d相反
\w 匹配数字+字母+_ \W 和\w相反
\s 匹配空白字符 \S 和\s相反
数量限定
*
匹配特定规则出现任意次
+
匹配特定规则出现至少1次? 匹配特定规则现出1或者0次(前面的字符可有可无)
{n} 匹配特定规则出现n次
{n,} 匹配特定规则出现至少n次
{n,m} 匹配特定规则出现n到m次
开关结尾限定
^ 匹配特定规则开关
$ 匹配特定规则结尾
范围限定
范围限定使用的[]
用法比较特殊,大概分为这么三种
1.连续范围
[0-9] 匹配数字 [a-z] 匹配小写字母 [3-8] 匹配3到8之间的数字
2.范围集合
[0-9a-zA-Z] 匹配数字+字母
3.无规律集合
[259a-f*] 匹配2/5/9+a到f和
*
中的任意字符4.取反
[^0-9] 匹配非数字
其它
. 匹配任意单个字符
\ 反斜杠 转义
() 分组
| 或者
异步任务
执行栈
执行栈,也称“调用栈”,是一种拥有 后进先出 的数据结构,被用来存储代码运行时创建的所有执行上顺序。
理解过程:
1.首先是全局的执行环境入栈
2.在全局环境下调用了first函数,再把first函数的环境压入栈中
3.在first函数里面调用了second函数,再把second函数的环境压入栈中
4.second执行完毕,于是把second的执行环境从栈中移除(先进后出,后入先出)
5.回到first的执行环境,再把fist的代码执行完成,从执行栈中再移除
6.最后把全局的执行环境也出栈,整个程序执行完成
EventLoop
Event Loop
即事件循环,是指浏览器或Node
的一种解决javaScript
单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
EventLoop和任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。
1.所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2.主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
3.一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
4.主线程不断重复上面的第三步。
Task(宏任务)和 MicroTask(微任务)
而在代码的执行过程中,我们还把所有的分为两个大类,宏任务和微任务
宏任务 | 微任务 |
---|---|
script环境 | Promise的then/catch回调 |
setInterval/setTimeout 定时器 | Object.observe(先忽略) |
reque stAnimationFrame 浏览器的帧循环(先忽略) | Proxy(先忽略) |
UI Rendering 浏览器的UI渲染(先忽略) |
Event Loop
中,每一次循环称为tick
,每一次tick
的任务如下:
执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去执行Task
(宏任务),每次宏任务执行完毕后,检查微任务(microTask
)队列是否为空,如果不为空的话,会先执行完微任务(microTask
)后,设置微任务(microTask
)队列为null
,然后再执行宏任务,如此循环