万字整合 JS高级

万字整合 JS高级

原型和原型链

一、前言

在js中,原型和原型链是一个很重要的知识点,只有理解了它,我们才能更深刻的理解js,在这里,我们将分成几个部分来逐步讲解。

二、构造函数

构造函数和普通函数本质上没什么区别,只不过使用了new关键字创建对象的函数,被叫做了构造函数。构造函数的首字母一般是大写,用以区分普通函数,当然不大写也不会有什么错误。

function Person(name, age) {
   	this.name = name;
   	this.age = age;
    this.species = '人类';
    this.say = function () {
        console.log("Hello");
    }
}

let per1 = new Person('xiaoming', 20);

三、原型对象

js 中,每一个函数类型的数据,都有一个叫做prototype的属性,这个属性指向的是一个对象,就是所谓的原型对象。
在这里插入图片描述

对于原型对象来说,它有个constructor属性,指向它的构造函数。

在这里插入图片描述

那么这个原型对象有什么用呢?最主要的作用就是用来存放 实例对象公有属性公有方法

在上面那个例子里species属性和say方法对于所有实例来说都一样,放在构造函数里,那每创建一个实例,就会重复创建一次相同的属性和方法,显得有些浪费。这时候,如果把这些公有的属性和方法放在原型对象里共享,就会好很多。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.species = '人类';
Person.prototype.say = function () {
    console.log("Hello");
}

let per1 = new Person('xiaoming', 20);
let per2 = new Person('xiaohong', 19);

console.log(per1.species); // 人类 
console.log(per2.species); // 人类

per1.say(); // Hello
per2.say(); // Hello

可是这里的species属性和say方法不是实例对象自己的,为什么可以直接用点运算符访问?这是因为在js中,对象如果在自己的这里找不到对应的属性或者方法,就会查看构造函数的原型对象,如果上面有这个属性或方法,就会返回属性值或调用方法。所以有时候,我们会用per1.constructor查看对象的构造函数:

console.log(per1.constructor); // Person()

这个constructor是原型对象的属性,在这里能被实例对象使用,原因就是上面所说的。那如果原型对象上也没有找到想要的属性呢?这就要说到原型链了。

四、原型链

说原型链之前,先来了解两个概念:

1. 显示原型

显示原型就是利用prototype属性查找原型,只是这个是函数类型数据的属性。

2. 隐式原型

隐式原型是利用__proto__属性查找原型,这个属性指向当前对象的构造函数的原型对象,这个属性是对象类型数据的属性,所以可以在实例对象上面使用

console.log(per1.__proto__ === Person.prototype); // true
console.log(per2.__proto__ === Person.prototype); // true

在这里插入图片描述

3. 原型链

既然这个是对象类型的属性,而原型对象也是对象,那么原型对象就也有这个属性,但是原型对象的__proto__又是指向哪呢?

我们来分析一下,既然原型对象也是对象,那我们只要找到对象的构造函数就能知道__proto__的指向了。而js中,对象的构造函数就是Object(),所以对象的原型对象,就是 Object.prototype。既然原型对象也是对象,那原型对象的原型对象,就也是Object.prototype。不过Object.prototype这个比较特殊,它没有上一层的原型对象,或者说是它的__proto__指向的是null (空)。

所以上面的关系图可以拓展成下面这种:

在这里插入图片描述

到这里,就可以回答前面那个问题了,如果某个对象查找属性,自己和原型对象上都没有,那就会继续往原型对象的原型对象上去找,这个例子里就是Object.prototype,这里就是查找的终点站了,在这里找不到,就没有更上一层了(null里面啥也没有),直接返回undefined。

可以看出,整个查找过程都是顺着__proto__属性,一步一步往上查找,形成了像链条一样的结构,这个结构,就是原型链。所以,原型链也叫作隐式原型链。

正是因为这个原因,我们在创建对象、数组、函数等等数据的时候,都自带一些属性和方法,这些属性和方法是在它们的原型上面保存着,所以它们自创建起就可以直接使用那些属性和方法。

五、函数也是一种对象

函数在js中,也算是一种特殊的对象,所以,可以想到的是,函数是不是也有一个__proto__属性?答案是肯定的,既然如此,那就按上面的思路,先来找找函数对象的构造函数。

在js中,所有函数都可以看做是Function()的实例,而Person()和Object()都是函数,所以它们的构造函数就是Function()。Function()本身也是函数,所以Function()也是自己的实例,听起来既怪异又合理,但是就是这么回事。

console.log(Person.constructor === Function); // true
console.log(Object.constructor === Function); // true
console.log(Function.constructor === Function); // true

既然知道了函数的构造函数,那么函数的__proto__指向我们也就知道了,就是Function.prototype。

console.log(Person.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true

在这里插入图片描述

六、总结

  1. 构造函数是使用了new关键字的函数,用来创建对象,所有函数都是Function()的实例
  2. 原型对象是用来存放实例对象的公有属性和公有方法的一个公共对象,所有原型对象都是Object()的实例
  3. 原型链又叫隐式原型链,是由__proto__属性串联起来,原型链的尽头是Object.prototype

箭头函数

前言

箭头函数作为ES6中新加入的语法,以其简化了我们的代码和让开发人员摆脱了“飘忽不定”的this指向等特点,深受广大开发者的喜爱,同时也深受面试官的喜爱,箭头函数常因其不同于普通函数的特点出现在各大公司的面试题中,so,本文会对箭头函数与普通函数进行一些分析。

介绍箭头函数(Arrow Function)

ES6中允许使用“箭头”(=>) 来定义函数。箭头函数相当于匿名函数,并且简化了函数定义。

我们来看一下如何使用 (=>) 来声明一个函数:

// 箭头函数
let foo = (name) => `我是${name}`
foo('张三') // 我是张三

// 等同于下面这个普通函数
let foo2 = function(name) {
    return `我是${name}`
}

箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }return

let foo = (name) => {
    if(name){
        return `我是${name}`
    }
    return '张三'
}
foo('张三') // 我是张三

⚠️这里需要注意的是如果箭头函数返回的是一个字面量对象,则需要用括号包裹该字面量对象返回

let foo = (name) => ({
    name,
    job: 'front end'
})
// 等同于
let foo2 = function (name) {
  return {
    name,
    job: 'front end'
  }
}

OK,箭头函数的基本介绍我们先看到这里,下面我们通过对比箭头函数与普通函数的区别来进一步了解箭头函数~

箭头函数与普通函数的区别

我们可以通过打印箭头函数和普通函数来看看两者到底有什么区别:

let fn = name => {
    console.log(name)
}
let fn2 = function(name) {
    console.log(name)
}
console.dir(fn) // 
console.dir(fn2) // 

在这里插入图片描述

从打印结果来看,箭头函数与普通函数相比,缺少了caller,arguments,prototype

声明方式不同,匿名函数

  • 声明一个普通函数需要使用关键字function来完成,并且使用function既可以声明成一个具名函数也可以声明成一个匿名函数
  • 声明一个箭头函数则只需要使用箭头就可以,无需使用关键字function,比普通函数声明更简洁。
  • 箭头函数只能声明成匿名函数,但可以通过表达式的方式让箭头函数具名

this指向不同

对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。

var name = '张三'
var person = {
    name: 'zhangsan',
    say: function() {
        console.log('say:',this.name)
    },
    say2: () => {
        console.log('say2:',this.name)
    }
}
person.say() // say: zhangsan
person.say2() // say2: 张三

这里第一个say定义的是一个普通函数,并且它是作为对象person的方法来进行调用的,所以它的this指向的就是person,所以它应该会输出`say: zhangsan

而第二个say2定义的是一个箭头函数,我们知道箭头函数本身没有this,它的this永远指向它定义时所在的上层作用域,所以say2this应该指向的是全局window,所以它会输出`say2: 张三

我们也可以通过Babel 转箭头函数产生的 ES5 代码来证明箭头函数没有自己的this,而是引用的上层作用域中this

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用的上层作用域中this

箭头函数的this永远不会变,call、apply、bind也无法改变

我们可以用call、apply、bind来改变普通函数的this指向,但是由于箭头函数的this指向在它定义时就已经确定了,永远指向它定义时的上层作用域中的this,所以使用这些方法永远也改变不了箭头函数this的指向。

var name = '张三'
var person = {
    name: 'zhangsan',
    say: function() {
        console.log('say:',this.name)
    },
    say2: () => {
        console.log('say2:',this.name)
    }
}

person.say.call({name:'小明'}) // say: 小明
person.say2.call({name:'小红'}) // say2: 张三

还是上面那个例子,只不过我们在调用的时候使用call试图改变this指向,第一个say是一个普通函数,它经过call调用,打印出的是say: 小明,这说明普通函数的this已经改变了,第二个say2是一个箭头函数,它也经过call调用,但它打印出的仍然是say2: 南玖,这就能够证明箭头函数的this永远不会变,即使使用call、apply、bind也无法改变

箭头函数没有原型prototype

let fn = name => {
    console.log(name)
}
let fn2 = function(name) {
    console.log(name)
}
console.log(fn.prototype) // undefined
console.dir(fn2.prototype) // {constructor: ƒ}

箭头函数不能当成一个构造函数

为什么箭头函数不能当成一个构造函数呢?我们先来用new调用一下看看会发生什么:

let fn = name => {
    console.log(name)
}

const f = new fn('zhangsan')

结果符合我们的预期,这样调用会报错

我们知道new内部实现其实是分为以下四步:

  • 新建一个空对象
  • 链接到原型
  • 绑定this,执行构造函数
  • 返回新对象
function myNew() {
// 1.新建一个空对象
let obj = {}
// 2.获得构造函数
let con = arguments.__proto__.constructor
// 3.链接原型
obj.__proto__ = con.prototype
// 4.绑定this,执行构造函数
let res = con.apply(obj, arguments)
// 5.返回新对象
return typeof res === 'object' ? res : obj
}

因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会变,并且箭头函数没有原型prototype,没法让他的实例的__proto__属性指向,所以箭头函数也就无法作为构造函数,否则用new调用时会报错!

没有new.target

new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,这个属性一般用在构造函数中,返回new调用的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,所以这个属性可以用来确定构造函数是怎么调用的。

function fn(name) {
    console.log('fn:',new.target)
}

fn('zhangsan') // undefined
new fn('zhangsan') 
/*
fn: ƒ fn(name) {
    console.log('fn:',new.target)
}
*/
let fn2 = (name) => {
    console.log('fn2',new.target)
}
fn2('nan') // 报错 Uncaught SyntaxError: new.target expression is not allowed here

⚠️注意:

  • new.target属性一般用在构造函数中,返回new调用的那个构造函数
  • 箭头函数的this指向全局对象,在箭头函数中使用new.target会报错
  • 箭头函数的this指向普通函数,它的new.target就是指向该普通函数的引用

箭头函数没有自己的arguments

箭头函数处于全局作用域中,则没有arguments
let fn = name => {
    console.log(arguments)
}
let fn2 = function(name) {
    console.log(arguments)
}
fn2() // Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
fn()  // 报错 Uncaught ReferenceError: arguments is not defined

还是用这两个函数来比较,普通函数能够打印出arguments,箭头函数使用arguments则会报错,因为箭头函数自身是没有arguments的,然后它会往上层作用域中去查找arguments,由于全局作用域中并没有定义arguments,所以会报错。

箭头函数处于普通函数的函数作用域中,arguments则是上层普通函数的arguments
let fn2 = function(name) {
    console.log('fn2:',arguments)
    let fn = name => {
        console.log('fn:',arguments)
    }
    fn()
}
fn2('zhangsan')

这里两个函数打印的arguments相同,都是fn2函数的arguments

可以使用rest参数代替

ES6 引入 rest 参数,用于获取函数不定数量的参数数组,这个API是用来替代arguments的,形式为...变量名,rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中

let fn3 = (a,...arr) => {
    console.log(a,arr) //1, [2,3,4,5,6]
}

fn3(1,2,3,4,5,6)

上面就是rest参数的基本用法,需要⚠️注意的是:

  • rest参数只能作为函数的最后一个参数

  • // 报错
    function f(a, ...b, c) {
      // ...
    }
    
    
  • 函数的length属性,不包括rest参数

rest参数与arguments的比较:

  • 箭头函数和普通函数都可以使用rest参数,而arguments只能普通函数使用
  • 接受参数restarguments更加灵活
  • rest参数是一个真正的数组,而arguments是一个类数组对象,不能直接使用数组方法

箭头函数不能重复函数参数名称

function fn(name,name) {
    console.log('fn2:',name)
}
let fn2 = (name,name) => {
    console.log('fn',name)
}
fn('zhang','san') // 'san'
fn2('zhang','san') // 报错

不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

这个可能是由于历史原因哈,TC39 在 2013 年和 2016 年分别讨论过两次,从*()*=>=*>=>* 中选出了=>*,勉强进入了 stage 1。而且因为有了异步生成器(async generator),所以还得同时考虑**异步箭头生成器(async arrow generator)**的东西,之前生成器 99.999% 的用途都是拿它来实现异步编程,并不是真的需要生成器本来的用途,自从有了 async/awaitgenerator生成器越来越没人用了。猜测可能是因为这个原因添加一个使用频率不高的语法,给规范带来较大的复杂度可能不值当。

Class类

class

class是一个语法糖,其底层还是通过 构造函数 去创建的。所以它的绝大部分功能,ES5 都可以做到。新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function() {
    return this.name;
}

const xiaoming = new Person('小明', 18);
console.log(xiaoming);

上面代码用ES6class实现,就是下面这样

class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
  
    sayName() {
      return this.name;
    }
}
const xiaoming = new Person('小明', 18)
console.log(xiaoming);
// { name: '小明', age: 18 }

console.log((typeof Person));
// function
console.log(Person === Person.prototype.constructor);
// true

constructor方法,这就是构造方法,this关键字代表实例对象。 类的数据类型就是函数,类本身就指向构造函数。

定义类的时候,前面不需要加 function, 而且方法之间不需要逗号分隔,加了会报错。

类的所有方法都定义在类的prototype属性上面。

class A {
    constructor() {}
    toString() {}
    toValue() {}
}
// 等同于
function A () {
    // constructor
};
A.prototype.toString = function() {};
A.prototype.toValue = function() {};

在类的实例上面调用方法,其实就是调用原型上的方法。

let a = new A();
a.constructor === A.prototype.constructor // true

constructor 方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

class A {
}

// 等同于
class A {
  constructor() {}
}

constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

class A {
  constructor() {
      return Object.create(null);
  }
}

console.log((new A()) instanceof A);
// false

类的实例

实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

注意:

  1. class不存在变量提升
new A(); // ReferenceError
class A {}

因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与继承有关,必须保证子类在父类之后定义。

{
  let A = class {};
  class B extends A {}
}

上面的代码不会报错,因为 B继承 A的时候,A已经有了定义。但是,如果存在 class提升,上面代码就会报错,因为 class 会被提升到代码头部,而let命令是不提升的,所以导致 B 继承 A 的时候,Foo还没有定义。

  1. this的指向 类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。 如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为"静态方法"。

class A {
    static classMethod() {
        return 'hello';
    }
}
A.classMethod();
console.log(A.classMethod());
// 'hello'

const a = new A();
a.classMethod();
// TypeError: a.classMethod is not a function

A 类的classMethod 方法前有 static关键字,表明这是一个静态方法,可以在 A 类上直接调用,而不是在实例上调用 在实例a上调用静态方法,会抛出一个错误,表示不存在改方法。

如果静态方法包含this关键字,这个this指的是类,而不是实例。

class A {
    static classMethod() {
      this.baz();
    }
    static baz() {
      console.log('hello');
    }
    baz() {
      console.log('world');
    }
}
A.classMethod();
// hello

静态方法classMethod调用了this.baz,这里的this指的是A类,而不是A的实例,等同于调用A.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。

父类的静态方法,可以被子类继承。

class A {
    static classMethod() {
        console.log('hello');
    }
}

class B extends A {}

B.classMethod() // 'hello'

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。 写法是在实例属性的前面,加上static关键字。

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}

继承

Class 可以通过extends关键字实现继承

class Animal {}
class Cat extends Animal { };

上面代码中 定义了一个 Cat 类,该类通过 extends关键字,继承了 Animal 类中所有的属性和方法。 但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Animal类。 下面,我们在Cat内部加上代码。

class Cat extends Animal {
    constructor(name, age, color) {
        // 调用父类的constructor(name, age)
        super(name, age);
        this.color = color;
    }
    toString() {
        return this.color + ' ' + super.toString(); // 调用父类的toString()
    }
}

constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

子类必须在 constructor 方法中调用 super 方法,否则新建实例就会报错。 这是因为子类自己的this对象,必须先通过 父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

class Animal { /* ... */ }

class Cat extends Animal {
  constructor() {
  }
}

let cp = new Cat();
// ReferenceError

Cat 继承了父类 Animal,但是它的构造函数没有调用super方法,导致新建实例报错。

如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

class Cat extends Animal {

}
// 等同于

class Cat extends Animal {
    constructor(...args) {
        super(...args);
    }
}

另一个需要注意的地方是,es5 的构造函数在调用父构造函数前可以访问 this, 但 es6 的构造函数在调用父构造函数(即 super)前不能访问 this。

class A {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class B extends A {
  constructor(x, y, name) {
    this.name = name; // ReferenceError
    super(x, y);
    this.name = name; // 正确
  }
}

上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

父类的静态方法,也会被子类继承。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world

super

super这个关键字,既可以当作函数使用,也可以当作对象使用

super作为函数调用

super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。

class A {
  constructor() {
    // new.target 指向正在执行的函数
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B

super作为对象调用

在普通方法中,指向父类的原型对象; 在静态方法中,指向父类

super对象在普通函数中调用
class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

如果属性定义在父类的原型对象上,super就可以取到。

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。

super对象在静态方法中调用

用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

const child = new Child();
child.myMethod(2); // instance 2

上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}

B.x = 3;
B.m() // 3

上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。

总结

  • class是一个语法糖,其底层还是通过 构造函数 去创建的。
  • 类的所有方法都定义在类的prototype属性上面。
  • 静态方法:在方法前加static,表示该方法不会被实例继承,而是直接通过类来调用。
  • 静态属性:在属性前加static,指的是 Class 本身的属性,而不是定义在实例对象(this)上的属性。
  • es5 的构造函数在调用父构造函数前可以访问 this, 但 es6 的构造函数在调用父构造函数(即 super)前不能访问 this。
  • super
    • 作为函数调用,代表父类的构造函数
    • 作为对象调用,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

Promise

含义

promise 是异步编程的一种解决方案,promise对象是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,从他获取异步操作的消息。

promise 对象的特点:

  • 对象的状态不受外界的影响。promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功) 和 rejected(已失败);
  • 一旦状态改变,就不会再改变,任何时候都可以得到这个结果;

缺点:

  • 无法取消promise,一旦新建他就会立即执行,无法中途取消
  • 如果不设置回调函数,promise内部抛出的错误,不会反应到外部
  • 当处于pending状态时,无法得知目前的状态是刚刚开始还是即将完成

基本用法

promise对象是一个构造函数,用来生成promise实例。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});


// 理解:promise构造函数接受俩个参数,分别是resolve 和reject。
js复制代码promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

// promise 实例生成后,可以使用then 方法 分别指定 resolved状态 和reject 状态的回调函数。

promise 新建后就会立即执行

let promise = new Promise(function (resolve,reject) {
        console.log("1,promise");
        resolve();
    })
    promise.then(function () {
        console.log('3,resolved.')
    })
    console.log("2,hello")


 //  promise 新建之后立即就会执行---所以就会立即执行 输出 1,promise
 //  then  方法指定的回调函数,将在当前脚本所有任务同步执行完成之后才会执行----输出 在 2 hello 之前按照顺序执行 和 1,hello
 //  当当前脚本执行完毕之后,then里的内容才会执行 ----  输出 3,resolved

调用 resolve或者reject 并不会终结 promise 的参数函数的执行。

new Promise((resolve, reject) => {
  resolve(1);
   return  resolve(1);  // 加上这行话 就不会在输出2
  console.log(2);
}).then(r => {
  console.log(r);
});
// 因为立即 resolved 的Promise 是在本轮事件循环的末尾执行,总是晚于本轮事件循环的同步任务。所以先输出2后输出1的内容

更好的写法:
new Promise((resolve, reject) => {
   return  resolve(1);  
}).then(r => {
  console.log(2);
  console.log(r);
});
输出的结果
//2
//1

Promise.prototype.then()

then方法是定义在原型对象Promise.prototype上的,为Promise 实例添加状态改变时的回调函数。 then方法返回一个新的promise实例,注意不是原来的Promise实例

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function (comments) {
  console.log("resolved: ", comments);
}, function (err){
  console.log("rejected: ", err);
});



// 等价写成箭头函数的写法
getJSON("/post/1.json").then(
  post => getJSON(post.commentURL)
).then(
  comments => console.log("resolved: ", comments),
  err => console.log("rejected: ", err)
);

Promise.prototype.catch()

含义

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

catch 捕获promise 抛出的异常的状态rejected或者是then 方法指定的回调函数在运行中抛出的错误,也会被catch方法捕获。

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

catch 返回一个promise对象,同时还可以继续调用then方法

const someAsyncThing = function () {
        return new Promise(function (resolve,reject) {
            // 因为 x 没有被声明定义,所以会报错
           // let x = 5;  // 如果声明了,则不会报错
            resolve(x +2);
        })
    }
    //调用someAsyncThing
    someAsyncThing().catch(function (error) {
        console.log('no no no',error)
    }).then(function () {
        console.log('carry on');
    })
// 因为 x 没有被声明定义,所以会报错 在运行完catch()方法指定的回调函数之后,会接着
// 运行后面的then()方法指定的回调函数。如果没有报错,则会跳过catch()方法。

promise.prototype.finally()

含义

  • finally()方法用于指定不管promise 对象最后的状态 如何,都会执行的操作。
  • finally 方法的回调函数不接受任何参数,所以finally方法里面的操作和promise 返回的状态无关,不依赖于promise的执行结果。

finally 本质是then方法的特例。

promise.finally(() => {
  // 语句
});

// 等同于
promise.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);


// 说明:如果不使用finally方法,同样的语句需要为成功和失败俩种情况各写一次。有了finally方法,则只需要写一次。

promise.all()

含义

promise.all() 方法用于将多个Promise 实例,包装成一个新的实例。

const p = Promise.all([p1, p2, p3]); 

promise.all() 方法接受一个数组作为参数,p1、p2、p3 都是他的实例。如果参数不是数组的话,但必须有iterator 的接口,且返回的每一个成员都是promise 实例。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

	const p1 = new Promise((resolve, reject) => {
        resolve('hello');
    })
        .then(result => result)
        .catch(e => e);

    const p2 = new Promise((resolve, reject) => {
        throw new Error('报错了');
    })
        .then(result => result)
        .catch(e => e);

    Promise.all([p1, p2])
        .then(result => console.log(result))
        .catch(e => console.log(e));
    // ["hello", Error: 报错了] ---- 这是rejected 输出的语句
    // Array(2)  0:hello 1:Error: 报错了   --- 这是resolv的执行结果

说明:上面代码中,p1resolvedp2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。

总结
all()是数组中的所有的对象变为resolve 只要有一个状态变为reject 则执行 all() 方法里的内容。
race()只要有一个实例发生改变时,P的总数组的状态就会随之改变。
allSettled()只有等待这些参数的实例都返回的结果,不管是fulfilled 还是rejected,包装实例才会结束。
any()只要参数所有的实例都变成fulfilled状态,包装的实例就会变成rejected 状态。如果所有的参数实例都变成rejected状态。包装实例就会变成rejected状态。

promise.resolve()

需要将现有的对象转为promise 对象,promise.resolve() 方法就起到这个作用。

参数是一个Promise 实例

如果参数是一个promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

参数是一个thenable 对象

thenable对象指的是具有then方法的对象。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function (value) {
  console.log(value);  // 42
});

 // 总结:promise.resolve()方法会将这个对象转为promise 对象,然后立即执行thenable 对象的then 方法
    // thenable 对象的then()方法执行后,对象P1状态就变为resolved。从而立即执行最后那个then方法指定的回调函数
     // 最后输出的结果为42

参数不是具有then方法的对象,或者根本就不是对象

const p = Promise.resolve('Hello'); // 字符串对象不具有then() 方法

p.then(function (s) {
  console.log(s)
});
// Hello

如果参数是一个原始值,或者是一个不具有then() 方法的对象,则Promise.resolve()方法返回一个新的Promise对象(从一生成的状态就是resolved,所以回调函数会立即执行),状态为resolved。

不带有任何参数

注:立即resolve()的promise 对象,是在本轮事件循环的结束时执行,而不是在下一轮的事件循环的开始时执行。

setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three

setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。

Proxy

含义

Proxy(代理),首先理解代理是什么意思,才能更便于了解Proxy的作用。

Proxy是一个代理,可以这么理解,就相当于它是一个快递代理点,快递会运输到该代理点,我们取件只需要去对应的代理点取件即可,代理点说快递没到就是没到,代理点说要出示证件就要出示证件。

Proxy代理的是一个对象,该对象被代理后我们就不能直接访问,需要通过代理访问。我们想要获取对象内的某个值,代理说有就有,说没有就没有,代理返回的值长这样,这个值它就是这样,这就是代理,也可以将它理解成拦截。

创建Proxy

语法:let proxy = new Proxy(target,handler)

new Proxy()用来生成Proxy实例

target参数表示要拦截的目标对象

handler参数是一个对象,用来制定拦截行为

栗子一
let figure = {
    name:'东方不败'
   }
// 创建代理的方法,当通过代理访问目标对象时,此对象中的对应方法会执行
let handlers = {
    get(target,prop){
        return '我是代理,我说返回什么就是什么'
   }
}

// 创建代理:代理的目标对象、代理的操作
let proxys = new Proxy(figure,handlers)
console.log(proxys.name);  // 我是代理,我说返回什么就是什么
console.log(proxys.age);  // 我是代理,我说返回什么就是什么

上面的例子就是利用Proxy,通过Proxy访问想要的值时,代理对取值进行拦截,返回给我们指定的信息,在这里,不论我们怎么取值,返回的都是我是代理,我说返回什么就是什么

栗子二
let proxy2 = new Proxy({},{
    get: function(target,propKey){
          return 10;
    }
})
proxy2.num = 20;
console.log(proxy2.num);  // 10
console.log(proxy2.name);  // 10

上面代码中,我们给Proxy两个参数,第一个参数:要代理的目标对象(也就是{}),第二个参数:配置对象(被代理对象的操作),在这里,配置对象有个get方法,用来拦截目标对象属性的访问请求。

通过Proxy访问属性的时候,get方法将访问的这一层拦截了,这里拦截函数返回的10,所以访问任何属性得到的都是10。也就是说,给目标对象添加了属性值,但是在访问这一层被拦截了,任何访问都会返回拦截的这一层。

get方法的两个参数分别是目标对象和所要访问的属性,这里没有做任何操作,直接返回10,所以我们获取的值都是10。

注意:如果想要Proxy生效,必须操作Proxy的实例,操作原对象是没有任何效果的。

栗子三

如果Proxy的第二个参数(配置对象)没有设置任何拦截,就等同于直接访问原对象。

let target = {}
let handler = {}
let proxy = new Proxy(target,handler)
proxy.time = 20;
console.log(proxy.time);  // 20
//handler没有设置任何拦截效果,访问proxy就等于访问target

对象内是可以设置函数的,可以将Proxy对象设置到object.proxy属性,这样就可以在object对象上调用

let object = { proxy : new Proxy(target,handler) }
// object.proxy调用

Proxy实例也可以作为其他对象的原型对象

let proxy = new Proxy({},{
    get : function(target,propKsy){
        return 10
    }
 })
 
let obj = Object.create(proxy)
obj.time = 10;  
console.log(obj);

在这里插入图片描述

上面这段代码中,proxyobj对象的原型,obj对象本身没有time属性,所以根据原型链,会在proxy对象上读取该对象。

Proxy的实例方法

get()

get()用于拦截某个属性的读取操作,可以接受三个参数:
1、目标对象
2、属性名
3、Proxy实例本身(操作指向的对象),该参数为可选参数。

let person = {
    name : '张三'
}
let proxy = new Proxy(person,{
    get:function(target,propKey){
    // 判断对象上是否存在该属性名
        if(propKey in target){
            // 存在,返回该属性
             return target[propKey]
         } else {
           // 不存在,抛出错误
              throw new ReferenceError("属性名:" + propKey + "不存在")
         }
    }
})
console.log(proxy.name);  // 张三
// console.log(proxy.age);  // Uncaught ReferenceError: 属性名:age不存在

in运算符:in操作符用来判断某个属性属于某个对象,可以是对象的直接属性,也可以是通过prototype继承的属性。

上面这段代码表示,如果访问目标不存在则抛出错误。如果没有设置拦截函数,访问不存在的属性会返回undefined

set()

set()方法用来拦截某个属性的赋值操作,接收四个参数,依次为:
1、目标对象
2、属性名
3、属性值
4、Proxy实例本身(可选)

栗子一

判断属性值是否大于200,大于200则报错,并且属性值必须是数字

let numHold = {
    set: function(obj,prop,value){
        if(prop === 'age'){
            if(!Number(value)){
                throw new TypeError('参数不是数字')
            }
            if(value > 200){
                throw new RangeError('参数不能大于200')
            }
        }
        // 如果条件满足则直接保存,将值赋值给该属性
        obj[prop] = value
   }
}
let persons = new Proxy({},numHold)
persons.age = 100
console.log(persons.age);  // 100
// persons.age = 300  // 报错,参数不能大于200
// persons.age = '东方'  // 报错,参数不是数字

上面代码中,设置了一个存值函数set,对对象的age属性进行赋值时,如果不满足age属性的赋值要求则会抛出一个相应的错误,其实这种写法就相当于是在做数据验证。

利用set方法还可进行数据绑定,每当数据发生变化时,自动更新DOM

同一个拦截器可以设置多个拦截操作,有时候,我们会在对象内部设置内部属性,该内部属性以下划线_开头,表示这些属性不应该被外部使用,结合getset方法,可以做到防止这些内部属性被外部读写。

// 拦截方法
let handler = {
    // 读取
    get(target,key){
       invariant(key,'get')
       return target[key]
    },
    // 写入
    set(target,key,value){
       invariant(key,'set');
       target[key] = value
       return true
        
    }
 }
 
 function invariant(key,action){
    // 属性名的第一个字符是_则为私有方法,不允许外部读写
    if(key[0] === '_'){
        throw new Error(`${action}私有属性${key}无效`)
    }
}

let target = {}  // 当前对象
let proxy = new Proxy(target,handler)
// proxy._prop;  // get私有属性_prop无效
// proxy._name = '东方不败' // set私有属性_name无效

apply()

apply方法拦截函数的调用,callapply操作,apply方法可以接受三个参数,分别是:
1、目标对象
2、目标对象的上下文对象(this)
3、目标对象的参数数组

栗子一

let target = function(){return '东方不败'}
let handler = {
    apply:function(){
        return '我是Proxy'
    }
}
let p = new Proxy(target,handler)
console.log(p());  // 我是Proxy

上面代码中,变量pProxy的实例,当它作为函数调用时,就会触发apply方法拦截,返回apply的返回结果

栗子二

let twice = {
    apply(target,ctx,args){
        // console.log(Reflect.apply(...arguments)); // 3
        return Reflect.apply(...arguments) * 2
    }
}
function sum(left,right){
    return left + right  // 1 + 2
}
let proxy6 = new Proxy(sum,twice)
console.log(proxy6(1,2)); // 6

面代码中,每当执行Proxy函数(直接调用或callapply调用),都会被apply方法拦截。

console.log(proxy6.call(null,2,3)); // 10

has()

has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性,该方法会生效。典型的操作就是in运算符。
has()方法接受两个参数:
1、目标对象
2、需要查询的属性名

// 使用has()方法隐藏某些属性,不被in运算符发现
let handler = {
    has(target,key){
        // key为传入的属性名,这里key[0]就是属性名的第一个字符
        if(key[0] === '_'){
           // 第一个字符是_则返回false
            return false
        }
        return key in target
    }
}
let target = { _prop:'foo',prop:'fuu' }
let proxy = new Proxy(target,handler)
console.log('_prop' in proxy);  // false  '_prop'属性不属于proxy对象
console.log('_prop' in target);  // true  '_prop'属性属于target对象

注意:
如果原对象不可配置或禁止扩展,这是has()拦截就会报错
虽然for...in循环也用到了了in运算符,但是has()拦截对for...in循环不生效。

construct()

construct()方法用于拦截new命令,当对Proxy实例使用new命令的时候触发
construct()接受三个参数:
1、目标对象
2、构造函数的参数数组
3、创建实例对象时,new命令作用的构造函数(也就是下面例子中的p2)

let con = {
    construct:function(target,arg){
        // target是一个函数(){}
        // args是一个参数数组
        // this : construct
        console.log(this === con); // true
        console.log('回调:'+arg.join(','));  // 回调:1,2
        return { value : arg[0] * 10 }  // construct返回的必须是一个对象,否则报错
        // return 1  // 返回的不是对象,报错'construct' on proxy: trap returned non-object ('1')
    }
}
let p = new Proxy(function(){},con)
console.log((new p(1,2).value));  // new p()触发construct拦截

在这里插入图片描述

注意:由于construct()拦截的是构造函数,所以它的目标对象必须是函数,否则会报错
注意:construct()中的this指向的是con,而不是实例对象

deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或返回false,当前属性就无法被delete命令删除,当对Proxy实例使用delete命令的时候触发

let del = {
    deleteProperty(target,key){
        invariant(key,'delete')
        delete target[key]  // 如invariant未抛出错误就证明是可以删除的,删除操作
        return true  // 抛出true
    }
}

function invariant(key,action){
    // 如果属性名的第一个字符是_说明是私有属性,抛出错误
    if(key[0] === '_'){
        throw new Error(`当前操作:${action}对于私有属性${key}无效`)
    }
}
let target = {_prop:'foo'}
let proxy = new Proxy(target,del)
// console.log(delete proxy._prop);  // 报错:当前操作:delete对于私有属性_prop无效

注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。

defineProperty()

defineProperty()方法拦截Object.defineProperty()操作

let h = {
    defineProperty (target,key,desc){
        // target 目标对象
        // 目标对象的属性名
        // desc目标对象的赋值
        return false
        // return target[key] = desc.value
    }
}
let t = {}
let pr = new Proxy(t,h)
console.log(pr.foo = 'bar');  // 不会生效,被拦截
console.log(t);  // {}

上面代码中,defineProperty()方法内部没有任何操作,只返回false,导致新添加的属性总是无效。
这里返回的false只是用来提示操作失败,本身并不能阻止添加新属性。

Proxy.revocable()

Proxy.revocable()方法返回一个可取消的 Proxy 实例。

let target = {}
let handler = {}
let {proxy, revoke} = Proxy.revocable(target , handler );

console.log(proxy.foo = 100);  // 100
revoke()  // 取消Proxy实例
console.log(proxy.foo); //  Cannot perform 'get' on a proxy that has been revoked

Proxy.revocable()返回一个对象,该对象内有proxy属性和revoke属性。
proxy是Proxy实例
revoke是一个函数,用来取消Proxy实例
上面代码中revoke执行完后,取消了Proxy实例,当再次访问Proxy实例时会报错。
Proxy.revocable()的一个使用场景:目标对象不允许直接访问,必须通过代理访问,一但访问结束,就是收回代理权,不允许再次访问。

this问题

虽然Proxy可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在Proxy代理的情况下,目标对象内部的this关键字会指向Proxy代理。

let target = {
   m : function () {
   console.log('proxy',this);  
   // m:() false
   // Proxy {m: ƒ} true
   console.log(this === proxy);  // target === proxy : false
    }
 }
 
let handler = {}
let proxy = new Proxy(target,handler)
target.m();  // target === proxy : false
proxy.m();  // proxy === proxy : true

正常情况下,对象内函数的this指向为对象本身,也就是上面代码的target,但是上面代码中Proxy代理了target,一旦proxy代理target对象,那么target.m()内部的this就是指向proxy,而不是target。所以,虽然proxy没有做任何拦截,但target.m()和proxy.m()会返回不一样的结果。

栗子一
由于this指向的变化,导致Proxy无法代理目标对象

let _name = new WeakMap()  // 将值保存在这里

class Person {
   constructor(name){
     _name.set(this,name)  // set一个_name等于name
   }
   get name(){
       return _name.get(this)  // 返回_name的值
  }
}

let jane = new Person('东方不败')
console.log(jane.name);  // 东方不败

let proxy2 = new Proxy(jane,{})
console.log(proxy2.name);  // undefined,这里的this指向的是Proxy,所以找不到值

上面代码中,目标对象东方不败的name属性,实际保存在外部WeakMap对象_name上面,通过this键区分。由于通过proxy2.name访问时,this指向proxy2,导致无法取到值,所以返回undefined。

此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以Proxy也无法代理这些原生对象的属性

let t =  new Date()
let h = {}
let p = new Proxy(t,h)
// console.log(p.getDate());  // 报错 this is not a Date object.
console.log(t.getDate());  // 8   

上面代码中,getData()方法只能在Date对象实例上面拿到,如果this不是Date对象实例就会报错。这时,this绑定原始对象,就可以解决这个问题。

let t2 = new Date('2023-01-01')

let h2 = {
    get(target,prop){
        if(prop === 'getDate'){
            // 更改this指向,绑定原始对象
            return target.getDate.bind(target)
        }
        return Reflect.get(target,prop);
    }
}
let p2 = new Proxy(t2,h2)
p2.getDate  // 8

bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()第一个参数的值。

另外,Proxy拦截函数内部的this,指向的是当前对象,对象内函数都是指向当前对象,其实就是对象内函数的this指向问题。

let handler = {
    get:function(target,key,receiver){
        return 'hello,'+key
    },
    set:function(target,key,value){
        console.log(this === handler );  // true
        target[key] = value
        return true
    }
 }
 
let proxy = new Proxy({},handler )
console.log(proxy.foo);  // hello,foo
proxy.foo = 1  // 触发set方法

Proxy支持的拦截操作

//1、get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。

//2、set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。

//3、has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。

//4、deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
        
//5、ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
       
//6、getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
        
//7、defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
       
//8、preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
       
//9、getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
       
//10、isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
       
//11、setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
       
//12、apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
       
//13、construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

在这里插入图片描述

<think>好的,我需要帮助用户理解PPO算法的详细内容、实现方式以及源码解析。首先,用户提到了参考中的两篇资料,其中一篇是通俗讲解RLHF和PPO实现的,另一篇是结合Python代码的PPO示例。我需要结合这些资料的信息,同时遵循用户给出的格式要求。 首先,我应该明确PPO的基本概念。PPO(Proximal Policy Optimization)是强化学习中的一种策略优化方法,属于策略梯度算法的一种改进。它的核心目标是提高训练的稳定性和样本效率,通过限制策略更新的幅度来避免训练过程中的剧烈波动。 接下来,用户要求详细介绍实现方式。根据参考中的资料,PPO有两种主要形式:PPO-Clip和PPO-Penalty。其中,PPO-Clip更常用,它通过引入一个剪切函数来限制新旧策略之间的差异,确保更新后的策略不会偏离旧策略太远。而PPO-Penalty则通过KL散度惩罚项来实现类似的限制。 然后,源码解析部分需要结合具体的代码示例。用户提供的参考中有使用PyTorch和Gym环境的代码示例,我需要解释代码中的关键部分,比如策略网络的结构、经验收集、优势估计、损失函数计算等。例如,在策略网络中,通常会使用Actor-Critic结构,Actor负责生成动作的概率分布,Critic评估状态的价值。 在实现步骤部分,我需要分步骤说明PPO的工作流程。首先是经验收集,使用当前策略与环境交互,收集状态、动作、奖励等数据。接着是优势估计,通常使用广义优势估计(GAE)来计算每个时间步的优势值。然后计算重要性采样比率,用于调整旧策略和新策略之间的差异。之后是损失函数的构建,包括策略损失、价值函数损失和熵正则项。最后进行梯度更新,并多次执行这些步骤直到收敛。 在代码解析中,我需要指出关键部分,比如剪切函数的应用,优势值的归一化处理,以及损失函数的具体实现。例如,在计算策略损失时,新旧策略的概率比会被剪切在1±ε的范围内,以防止过大的更新。 最后,根据用户的要求,生成相关问题。这些问题应该围绕PPO的改进方法、参数调整、与其他算法的比较以及实际应用中的挑战等方面展开。 需要确保整个回答符合格式要求:行内数学公式用$...$,独立公式用$$...$$,使用中文,结构清晰,引用标识自然添加。同时,避免使用过多的专业术语,保持通俗易懂,让用户能够逐步理解PPO的实现细节。</think>### PPO算法详解与实现解析 #### 一、算法基础 PPO(Proximal Policy Optimization)是一种基于策略梯度的强化学习算法,核心思想是通过约束策略更新的幅度来保证训练稳定性。其核心公式为: $$L^{CLIP}(\theta) = \mathbb{E}_t \left[\min\left( r_t(\theta)A_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon)A_t \right)\right]$$ 其中$r_t(\theta)=\frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}$是重要性采样比率,$\epsilon$是剪切系数(通常取0.1-0.3)[^1]。 #### 二、实现步骤 1. **经验收集**:使用当前策略$\pi_{\theta_{old}}$与环境交互,收集轨迹数据$(s_t,a_t,r_t,s_{t+1})$ 2. **优势估计**:计算广义优势估计(GAE): $$A_t = \delta_t + (\gamma\lambda)\delta_{t+1} + \cdots + (\gamma\lambda)^{T-t+1}\delta_{T-1}$$ 其中$\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)$ 3. **重要性采样**:计算新旧策略概率比$r_t(\theta)$ 4. **损失计算**: - 策略损失:$L^{CLIP}$ - 价值损失:$(V_\theta(s_t) - R_t)^2$ - 熵正则项:$-\beta H(\pi_\theta(\cdot|s_t))$ 5. **梯度更新**:使用Adam优化器联合优化总损失$L_{total} = L^{CLIP} + c_1 L^{VF} + c_2 L^{Entropy}$ #### 三、代码解析(基于PyTorch) ```python import torch import torch.nn as nn from torch.optim import Adam class ActorCritic(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.actor = nn.Sequential( # 策略网络 nn.Linear(obs_dim, 64), nn.Tanh(), nn.Linear(64, act_dim)) self.critic = nn.Sequential( # 价值网络 nn.Linear(obs_dim, 64), nn.Tanh(), nn.Linear(64, 1)) def forward(self, x): return torch.distributions.Categorical(logits=self.actor(x)), self.critic(x) ``` #### 四、训练流程关键代码 ```python def update(self, obs, acts, advs, returns, old_log_probs): # 计算新策略的概率分布 dist, values = self.ac(obs) new_log_probs = dist.log_prob(acts) # 重要性采样比率 ratio = (new_log_probs - old_log_probs).exp() # 剪切策略损失 surr1 = ratio * advs surr2 = torch.clamp(ratio, 1.0 - self.eps, 1.0 + self.eps) * advs policy_loss = -torch.min(surr1, surr2).mean() # 价值损失 value_loss = 0.5 * (returns - values).pow(2).mean() # 熵正则项 entropy_loss = -dist.entropy().mean() # 总损失 loss = policy_loss + 0.5*value_loss + 0.01*entropy_loss # 梯度更新 self.optimizer.zero_grad() loss.backward() self.optimizer.step() ``` 关键参数说明: - `advs`:经过归一化的优势估计值 - `returns`:实际累积回报 - `self.eps`:剪切系数(默认0.2) - 优化器通常设置学习率3e-4[^2]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值