javascript学习笔记

70 阅读12分钟

全局预编译 函数预编译

全局预编译
  • 查找变量的声明,作为GO对象的属性名,值为undefined
  • 查找函数声明,作为GO对象的属性名,值为undefined
<!DOCTYPE html>
<html>
<head>
</body>
<script>
console.log(a)   //function
var a = 100
console.log(a)  // 100
function a(){
	console.log(111)
}
console.log(a)  // 100
a()   //Uncaught TypeError: a is not a function 
</script>
</html>

分析

  1. 生产window对象
  2. 查找变量的声明,把a作为window对象的属性名,属性值为undefined
  3. 查找函数的声明,把函数名为a作为window对象的属性名,属性值为function

当预编译结束,代码从上而下进行执行,执行到第6行,此时a为function 所以输出为 function; 执行到第7行 a=100,a被赋值为100;执行到第8行,输出100; 9-11行跳过,执行12行,这时a为100,所以不能将a作为函数进行调用

注意:为什么不执行9-11行;因为(function a() { console.log(111) }会被预编译并提升到作用域顶部)因此,在执行代码之前,函数声明已经被处理

结论: 如果存在同名的变量和函数,函数的优先级高

函数预编译
  1. 在函数被调用时,为当前函数产生AO对象
  2. 查找形参和变量声明作为AO对象的属性名,值为undefined
  3. 使用实参的值改变形参的值
  4. 查找函数声明,作为AO对象的属性名,值为function
<script>
function a(b,c){
	console.log(b) //function
	var b = 0
	console.log(b) //0
	function b(){
		console.log(222)
	}
	console.log(c) //undefined
}
a(1)

/*
  	AO:{
  		1.先看形参 
  		b:undefined
  		c:undefined
 		2.看变量
  		var b = 0 因为上面b已经存在,所以这行忽略
  		3.看函数
  		function b(){
			console.log(222)
		}
		因为上面b已经存在,只需要把b改成 function
		预编译结束
  	}
 
 */
</script>

变量提升

  • 变量声明的提升:在javascript中,使用var声明的变量会在其作用域内被提升到作用域的顶部,这意味着你可以在变量声明之前引用该变量,而不会报错,但是,变量的赋值操作并不会被提升,只有变量的声明部分会被提升。
console.log(x) //输出undefined
var x = 10

上述代码中,变量x在声明之前被引用,但是他的值是undefined,因为赋值操作并没有被提升。

  • 函数声明的提升:与变量声明不同,使用function声明的函数会被完整的提升到作用域的顶部,包括函数的定义和函数体内的代码,这意味着你可以在函数声明之前调用该函数,而不会引发错误。
myFunction()  //Hello!
console.log(x) //Uncaught ReferenceError: a is not defined
function myFunction(){
   console.log(x) //undefined
   var x  = 10 
   console.log("myFunction")
}

但是需要注意:函数内部的变量同时存在变量提升,但是仅仅会提升到函数顶部,如上: 在函数内部可以调用x, 但是在函数外部调用x 会报错。全局的变量提升 只有 var 和 function ,而 let 和 const 是不会有变量提升的

原型

什么是原型对象

JavaScript 常被描述为一种基于原型的语言 (prototype-based language) ——每个对象拥有一个原型对象

原型对象的获取

Object.getPrototypeOf()  静态方法返回指定对象的原型(即内部 [[Prototype]] 属性的值)。

var obj = {}
obj.__proto__
Object.getPrototypeOf(obj)
console.log(obj.__proto__ === Object.getPrototypeOf(obj)) //true
var arr = []
arr.__proto__
Object.getPrototypeOf(arr)
console.log(arr.__proto__ === Object.getPrototypeOf(arr)) //true
//在使用 `new` 运算符调用函数时,构造函数的 `prototype` 属性将成为新对象的原型。
function Ctor() {}
const inst = new Ctor();
console.log(Object.getPrototypeOf(inst) === Ctor.prototype); // true

总结:实际开发中,上述对象,数组,函数使用频率最高,获取原型对象可以通过废弃的__proto__获取 也可以使用Object.getPrototypeOf()获取,构造函数的使用和对象,数组有不同之处,下面会单独讲解

注意:构造函数有个属性prototype,这是构造函数特有的,而上述其他类型则没有这个prototype属性

构造函数与原型对象的关系
  • 在Person构造函数的内部存在一个属性prototype指向Person的原型对象
  • 在Person原型对象的内部也存在一个属性constructor指向Person的构造函数

截屏2023-12-15 11.30.57.png

证明Person构造函数中存在prototype属性
function Person(n){
	this.uname = n
}
//打印构造函数的结构
console.dir(Person)

截屏2023-12-15 11.28.32.png

由上图可以得出Person构造函数中存在prototype属性,该属性指向一个对象,这个对象称为Person的原型对象

console.log(Person.prototype.constructor === Person) //true

我们可以通过 Person.prototype.constructor 来获取原型对象的 constructor 属性,并将其与 Person 构造函数进行比较。如果它们相等,则说明 Person 原型对象的 constructor 属性指向 Person 构造函数

结论

通过以上验证,可以证明 Person 构造函数的 prototype 属性指向 Person 的原型对象,而 Person 的原型对象的 constructor 属性指向 Person 构造函数

实例对象与原型对象的关系
function Person(n){
	this.uname = n
}
var p1 = new Person("张三")
console.dir(p1)

截屏2023-12-15 13.46.35.png

Person实例化出来的实例对象p1中存在__proto__属性指向Person的原型对象

使用原型定义方法
//在构造函数中定义属性
function Person(u,a){
	this.uname = u
	this.age = a
}
//在原型中定义方法
Person.prototype.sayHi = function(){
	console.log("sayHi")
}
const p1 = new Person("张三",14)
const p2 = new Person("李四",20)
console.log(p1.sayHi === p2.sayHi) //true
p1.sayHi() //sayHi
p2.sayHi() //sayHi

我们看到p1和p2中虽然没有sayHi方法,但是都可以调用sayHi方法

总结:x.__proto__ === y.prototype 其中 x需要是y 搞出来的,例子如下

function y(){}
var x = new y()
console.log(x.__proto__ === y.prototype) // true
console.log(y.__proto__ === Function.prototype) //true
console.log(y.prototype.__proto__ === Object.prototype) //true
y.prototype.__proto__ === x.__proto__.__proto__ === Object.prototype
//其中console.dir(x.__proto__) 为 Object 所以上面第5行也成立

原型链

在 JavaScript 中,每个对象都有一个原型对象。原型对象也是一个对象,它可以拥有自己的原型对象,从而形成一个原型链。原型链是一种对象之间的关联机制,它允许对象继承另一个对象的属性和方法。 以下是原型链的定义和工作原理:

  1. 每个对象都有一个隐式的 __proto__ 属性,它指向该对象的原型对象。原型对象也是一个对象,同样有一个 __proto__ 属性,指向它的原型对象。这样一直往上追溯,直到一个对象的原型对象为 null,形成了原型链的顶端。
  2. 当我们访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端(即原型对象为 null)。
  3. 如果在原型链上的某个对象上找到了属性或方法,就会使用该属性或方法。如果在整个原型链上都没有找到该属性或方法,那么返回 undefined

this指向

全局作用域
<script>
console.log("this1:",this)
</script>

在全局作用域下,this指向的就是window对象

方法体中
<script>
function test(){
	console.log("this2:",this)
}
//函数的独立调用
test()

var obj = {
	a:0,
	test:function(x){
		console.log("this3:",this)
	}
}
//对象调用
obj.test()

//箭头函数
var jiantou = {
	a:0,
	test:(x) =>{
		console.log("this4:",this)
	}
}
jiantou.test()
</script>

截屏2023-12-18 09.32.50.png

在方法体中,this的指向调用者,谁调用它,它就指向谁。但是ES6中的箭头函数没有自己的this绑定,他会继承外层作用域的this值

定时器
<script>
setTimeout(() => {
	console.log("this5:",this)
}, 1000);
setTimeout(function() {
	console.log("this6:",this)
}, 1000);

var time = {
	a:0,
	test:function(x){
		setTimeout(() => {
			console.log("this7:",this)
		}, 1000);
		setTimeout(function() {
			console.log("this8:",this)
		}, 1000);
	}
}
time.test()
</script>

截屏2023-12-18 09.39.05.png

setTimeout(function() {}, 1000)指向window对象,setTimeout(() => {}, 1000);箭头函数的定时器遵循:箭头函数没有自己的this绑定,他会继承外层作用域的this值

数组中
var arr = ["1","2","3"]
arr.forEach(function(item,index){
	console.log("this9:",this)
})

var arrObj = {
	inArr : ["1","2","3"],
	test:function(x){
		this.inArr.forEach(function(item,index){
			console.log(this.inArr) //undefined
			console.log("this10:",this)
		})
	}
}
arrObj.test()

截屏2023-12-18 10.09.26.png

数组中forEach,findIndex,map的指向也是window

this指向的改变
call
<script>
function greet(){
	console.log(this)
}
var person = {
	name:"张三"
}
greet()
greet.call(person)

function sum(a,b){
	console.log(this)
	console.log(a+b)
}
sum(1,2)
sum.call(person,4,5) //call支持传递参数,放在第一个参数后面
</script>

截屏2023-12-18 10.51.24.png

使用call()方法,将greet函数中的this指向了person对象,从而改变了函数中this的指向

apply
</script>
function sum(a,b){
	console.log(this)
	console.log(a+b)
}
sum(1,2)
sum.apply(person,[4,5])
</script>

截屏2023-12-18 10.58.15.png

apply和call的用法一样,只不过在传参数时,第一个参数一样,后面参数apply使用数组接收

bind
function greet(a,b){
	console.log(this)
	console.log(a+b)
}
var person = {
	name:"张三"
}
const greetPerson = greet.bind(person)
greet(1,2)
greetPerson(1,2)

截屏2023-12-18 11.03.35.png

在使用bind()方法时,会创建一个新的函数greetPerson,该函数中的this指向了person对象,从而改变了函数中this的指向

Promise

1 为什么需要promise

实际开发中,在使用ajax时,会进行嵌套的调用,从而产生回调地狱,promise就是为了解决回调地狱的问题

2 promise的基本使用

promise是一个构造函数,通过new关键字实力化对象

new Promise((resolve,reject) =>{})
  • promise 接受一个函数作为参数
  • 在参数函数中接受两个参数(resolve reject)
  • resolve: 成功函数
  • reject: 失败函数

promise实例有两个属性

  • state:状态
  • result:结果
3 promise的状态
  • 第一种状态 pending(准备,待解决,进行中)
  • 第二种状态 fulfilled(已完成,成功)
  • 第三种状态 rejected(已拒绝,失败)
4 promise状态的改变

通过调用resolve()和reject()改变当前的promise对象的状态

const p = new Promise((resolve,reject) =>{
    //resolve():调用函数,使当前promise对象的状态改成fulfilled
    resolve()
})
console.dir(p) //fulfilled
  • resolve():调用函数,使当前的promise对象的状态改成fulfilled
  • reject():调用函数,使当前的promise对象的状态改成rejected

注意:promise状态的改变是一次性的

const p = new Promise((resolve,reject) =>{
    resolve()
    reject()
})
console.dir(p) //fulfilled
5 promise的结果
const p = new Promise((resolve,reject) =>{
	//通过调用resolve,传递参数,改变当前promise对象的结果
	resolve("成功的结果")
	//reject("失败的结果")
})
console.dir(p) 
6 then方法的参数

then方法有两个参数

  • 是一个函数
  • 还是一个函数
const p = new Promise((resolve,reject) =>{
	resolve("成功的结果")
})
p.then(()=>{
	console.log("成功时调用")
},()=>{
	console.log("失败时调用")
})
7 then方法参数的获取
const p = new Promise((resolve,reject) =>{
	resolve("成功的结果")
})
p.then((value)=>{
	console.log(value)
},(reason)=>{
	console.log(reason)
})
8 then方法的返回值

then方法的返回值:是一个promise对象

const p = new Promise((resolve,reject) =>{
	resolve("成功的结果")
})
const t = p.then((value)=>{
	console.log(value)
},(reason)=>{
	console.log(reason)
})
console.dir(t) //Promise
9 返回实例的状态改变
const p = new Promise((resolve,reject) =>{
	resolve("成功的结果")
})
const t = p.then((value)=>{
	console.log(value)
    //使用return 将t的实例状态改成fulfilled
	return 123
},(reason)=>{
	console.log(reason)
	return 456
})

t.then((value)=>{
	console.log(value)
},(reason)=>{
	console.log(reason)
})
10 catch方法
new Promise((resolve,reject)=>{
	reject("失败")
}).then((value)=>{
	console.log(value)
}).catch((reason)=>{
	console.log(reason) //失败
})
11 实际应用
function getData(url,data={}){
	return new Promise((resolve,reject)=>{
		$.ajax({
			type:"GET",
			url:url,
			dataType:"json",
			data:data,
			success:function(res){
				resolve(res)
			},
			error:function(res){
				reject(res)
			}
		})
	})
}
getData("https://www.imooc.com/activity/servicetime1").then((data)=>{
	console.log("成功:",data)
	const {id} = data.result
	return getData("https://www.imooc.com/common/adver-getadver")
}).then((data)=>{
	return getData("https://www.imooc.com/common/activity-grantcoupon")
}).then((data)=>{
	console.log(data)
}).catch((resason)=>{
	console.log("失败:",resason)
})

链式调用时,只需要写一个catch,不管哪个发生错误,都会进入catch

闭包

  • 如果在内部函数使用了外部函数的变量,就会形成闭包,闭包保留了外部环境的引用
  • 如果内部函数被返回到了外部函数的外面,在外部函数执行完后,依然可以使用闭包里的值
闭包的形成

在内部函数使用外部函数的变量,就会形成闭包,闭包是当前作用域的延伸

function a(){
	var aa = 100
	function b(){
            console.log(aa)
	}
	b()
}
a()

b函数内部,使用了a函数里面的aa变量,所以形成了闭包

function a(){
	var aa = 100
	function b(){
		console.log(b)
	}
	b()
}
a()

b函数内部,使用了a函数里面的b函数(变量),所以形成了闭包

function a(){
	var aa = 100
	function b(){
		var b = 200
		console.log(b)
	}
	b()
}
a()

b函数内部,使用了自身内部的b变量,没有形成闭包

闭包的保持

如果希望在函数调用后,闭包依然保持,就需要将内部函数返回到外部函数的外部。

function a(){
	var num = 100
	function b(){
		console.log(++num)
	}
	return b
}

var demo = a() //此时 demo = b b并没有执行
console.dir(demo)
demo() // 101
demo() // 102
闭包的作用
  • 在函数外部访问函数内部变量
  • 延长变量生命周期
  • 实现回调和异步操作

一般来说,在函数的外部是没有办法访问函数内部的变量的,设计闭包最主要的作用就是为了解决这个问题


<script>
function createCounter() {
  var count = 100;
  function getCount(){
      return count;
  }
  return getCount
}
const counter = createCounter();
console.log(counter.count) //undefined 函数外部无法访问函数内部的变量
console.log(counter()) //100

//上面代码往往这么来写
function createCounter() {
  var count = 100;
  return {
      getCount: function() { 
          return count; 
      }
  }
}
</script>

<script>
function a(){
	var x = 0;
	function y(){
		console.log(++x)
	}
	return y
}
var f = a()
f()
f()
</script>
//第9行调用a函数,将内部y函数返回,保存在函数a的外部,形成闭包
//第10-11行调用f函数,实质上是调用内部函数,在函数y的[[scopes]]属性中可以找到闭包对象,从而访问到里面的值

CommonJS AMD CMD ESModule

CommonJS

commonJS规范模块加载是同步的,服务端加载的模块从内存或者磁盘加载,耗时基本可以忽略,所以在服务的开发语言nodejs中完全适用,但是到了浏览器端就不行了,由于网络加载存在延迟,多个js如果存在前后依赖,则很难保证加载顺序,所以commonJS规范不适用浏览器端

//test.js
function sum(a,b){
    return a+b;
}

function sub(a,b){
    return a-b;
}

module.exports = {
    sum,
    sub
}

//index.js
const math = require("./test")
const r1 = math.sum(1,2);
const r2 = math.sub(2,1);
console.log(r1);
console.log(r2);
//直接使用 node index.js 运行 
AMD CMD

这两个存活时间较短,已经被淘汰

ESModule

es6的两种导出方式

  • 单独导出某个模块
//test.js
export function sum(a,b){
    return a+b
}

export function sub(a,b){
    return a-b
}

//index.js
import { sum,sub } from "./test";
const r1 = sum(1,2)
const r2 = sub(2,1)
alert(r1)
alert(r2)
  • 整体模块导出
//test.js
const user = {
    sum: function sum(a, b) {
        return a + b
    },
    name: "张三",
    age:20
}

export default user;

//index.js
import user from "./test";
const r1 = user.sum(1,2)
const name = user.name
const age = user.age

单独导出和整体导出使用区别

单独导出的两种写法
  • 第一种
//1.js中导出
function sum(a,b){
    return a+b 
}

function sub(a,b){
    return a-b 
}

module.export = {
    sum,
    sub
}

//app.vue 使用时需要将导出内容用{}包裹
import { sum,sub } from "./1.js";
sum(1,3)
sub(4,1)

  • 第二种
//1.js中导出
export function sum(a,b){
    return a+b
}
export function sub(a,b){
    return a-b
}

//app.vue 使用时需要将导出内容用{}包裹
import { sum,sub } from "./1.js";
sum(1,3)
sub(4,1)

总结:单独导出时,在使用时,导出内容需要用{}包裹

整体导出
//test.js
const user = {
    sum: function sum(a, b) {
        return a + b
    },
    name: "张三",
    age:20
}

export default user;

//index.js
import user from "./test";
const r1 = user.sum(1,2)
const name = user.name
const age = user.age

总结:整体导出,在使用时,导出内容不需要用{}包裹