原型

Javascript 规定,每一个函数都有一个 prototype 属性,属性值是一个对象,这个对象就叫做原型(原型对象),这个对象的所有属性和方法,都会被构造函数的实例继承

这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上

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

console.log(Person.prototype)
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person(...)
var p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // true

这时所有实例的 sayName() 方法,其实都指向同一个内存地址

__proto__

任意对象都有 __proto__ 属性,这个属性指向了构造函数的 prototype 属性,也就是原型对象

获取原型对象:

  • 通过 构造函数.prototype 可以获取
  • 通过 实例.__proto__ 可以获取(隐式原型)
  • 它们指向了同一个对象 构造函数.prototype === 实例.__proto__

注意:__proto__是浏览器的一个隐藏(私有)属性,IE 浏览器不支持,不要通过它来修改原型里的内容,如果要修改原型中的内容,使用 构造函数.prototype 去修改

constructor 属性

默认情况下,原型对象中只包含了一个属性:constructor,constructor 属性指向了当前原型对象的构造函数

function Person() {}

console.log(Person.prototype)
console.log(Person.prototype.constructor) // 构造函数本身

var p = new Person()
console.log(p)
// p 实例对象没有constructor 属性, 该属性来源于原型上
console.log(p.constructor == Person.prototype.constructor) // true

构造函数、实例、原型三者之间的关系

构造函数:构造函数就是一个函数,配合 new 可以新建对象

实例:通过构造函数实例化出来的对象我们把它叫做构造函数的实例。一个构造函数可以有很多实例

原型:每一个构造函数都有一个属性prototype,函数的 prototype 属性值就是原型。通过构造函数创建出来的实例能够直接使用原型上的属性和方法

原型三角关系:

  • 构造函数和原型

    • 构造函数,通过 prototype 属性访问原型
    • 原型通过 constructor 属性访问到构造函数
  • 构造函数 和 实例对象

    • 构造函数可以创建实例对象
    • 实例对象不能直接访问到构造函数
  • 原型 和 实例对象关系

    • 实例对象可以直接访问到原型上的所有成员
    • 实例对象可以间接的访问到构造函数(通过原型上的 constructor 属性)

原型链

任何一个对象,都有原型对象,原型对象本身又是一个对象,所以原型对象也有自己的原型对象,这样形成的链式结构,就是原型链

绘制对象的原型链结构:

var p = new Person()
// p ==> Person.prototype ==> Object.prototype ==> null
var o = new Object()
// o ==> Object.prototype ==> null
var arr = new Array()
// arr ==> Array.prototype ==> Object.prototype ==> null
var date = new Date()
// date ==> Date.prototype ==> Object.prototype ==> null

// Math 是个内置对象,不是个构造函数
// Math ==> Object.prototype ==> null

总结:Object.prototype 是原型链的尽头,Object.prototype 的原型是 null

函数的原型链结构

函数是由 new Function 创建出来的,因此函数也是一个对象,所有的函数都是 Function 的实例

Person ==> Function.prototype ==> Object.prototype ==> null

Function.prototype 类型是个函数

完整版原型链

图一

图二

  1. 所有函数都是 new Function 创建出来的,因此 所有函数.__proto__ 都是 Function.prototype
  2. 所有对象都是 new Object 创建出来的,因此 所有对象.__proto__ 都是 Object.prototype

参考链接

ES5实现继承的六种方式

原型链

利用原型链让一个引用类型继承另一个引用类型的属性和方法。

function SuperType () {
this.property = true;
}

SuperType.prototype.getSuperValue = function () {
return this.property;
};

// 子类 SubType
function SubType () {
this.subProperty = false;
}

SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subProperty;
};

// 实例
var instance = new SubType();
console.log(instance);
console.log(instance.getSuperValue()); // true
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof Object); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(Object.prototype.isPrototypeOf(instance)); // true

缺点:

  1. 来自原型对象的引用属性是所有实例共享的。
  2. 创建子类实例时,无法向父类构造函数传参。

举例如下:

// 1. 来自原型对象的引用属性是所有实例共享的

// 父类
function SuperType () {
this.colors = ['red', 'blue', 'green'];
}

// 子类
function SubType () {

}
SubType.prototype = new SuperType();

// 实例
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'blue', 'green', 'black']

// 因为修改colors是修改的SubType.prototype.colors,所以所有的实例都会更新
// 2. 创建子类实例时,无法向父类构造函数传参

// 调用父类是在 SubType.prototype = new SuperType()
// 新建子类实例调用 new SubType()
// 所以无法再new SubType() 的时候给父类 SuperType() 传参

借用构造函数

在子类构造函数的内部通过call()以及apply()调用父类构造函数。

// 父类 SuperType
function SuperType (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];

this.getName = function () {
return this.name;
}
}

// 子类
function SubType (name) {
// 继承了SuperType,同时还传递了参数
SuperType.call(this, name);

// 实例属性
this.age = 20;
}

// 实例
var instance1 = new SubType('Tom');
instance1.colors.push('black');
console.log(instance1.name); // "Tom"
console.log(instance1.getName()); // "Tom"
console.log(instance1.age); // 20
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
var instance2 = new SubType('Peter');
console.log(instance2.name); // "Peter"
console.log(instance2.getName()); // "Peter"
console.log(instance2.age); // 20
console.log(instance2.colors); // ['red', 'blue', 'green']

可以看到,借用构造函数实现继承,解决了原型链继承的两个问题,既可以在新建子类实例的时候给父类构造函数传递参数,也不会造成子类实例共享父类引用变量。

但是你注意到了吗,这里我们把父类方法也写在了SuperType()构造函数里面,可以像前面一样写在SuperType.prototype上吗?

答案是不可以,必须写在SuperType()构造函数里面。因为这里是通过调用SuperType.call(this)来实现继承的,并没有通过new生成一个父类实例,所以如果写在prototype上,子类是无法拿到的。

缺点:如果方法都在构造函数中定义,那么就无法复用函数。每次构建实例时都会在实例中保留方法函数,造成了内存的浪费,同时也无法实现同步更新,因为每个实例都是单独的方法函数。如果方法写在prototype上,就只会有一份,更新时候会做到同步更新。

组合继承

将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

// 父类
function SuperType (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function () {
console.log(this.name);
}

// 子类
function SubType (name, age) {
// 继承父类实例属性
SuperType.call(this, name);

// 子类实例属性
this.age = age;
}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
console.log(this.age);
};

// 实例
var instance1 = new SubType('Tom', 20);
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
instance1.sayName(); // "Tom"
instance1.sayAge(); // 20

var instance2 = new SubType('Peter', 30);
console.log(instance2.colors); // ['red', 'blue', 'green']
instance2.sayName(); // "Peter"
instance2.sayAge(); // 30

缺点: 调用了两次父类构造函数,一次通过SuperType.call(this)调用,一次通过new SuperType()调用。

原型式继承

不使用严格意义上的构造函数,借助原型可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型。

// 在object函数内部,先创建了一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。
// 从本质上讲,object()对传入其中的对象执行了一次浅复制。

function object (o) {
function F() {}
F.prototype = o;
return new F();
}


var person = {
name: 'Tom',
friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

console.log(anotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
console.log(yetAnotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']

Object.create()在传入一个参数的情况下与前面写的object()方法的行为相同

var person = {
name: 'Tom',
friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = Object.create(person, {
name: {
value: 'Linda',
enumerable: true
}
});
yetAnotherPerson.friends.push('Barbie');

console.log(anotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
console.log(yetAnotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']

缺点:和原型链继承一样,所有子类实例共享父类的引用类型。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,创建一个仅用于封装继承过程的函数,该函数内部以某种形式来做增强对象,最后返回对象

function object (o) {
function F() {}
F.prototype = o;
return new F();
}

function createAnother (o) {
var clone = object(o);
clone.sayHi = function () {
console.log('Hi');
}
return clone;
}

var person = {
name: 'Tom',
friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "Hi"
anotherPerson.friends.push('Rob');
console.log(anotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob']
var yerAnotherPerson = createAnother(person);
console.log(yerAnotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob']

缺点: 和原型链式继承一样,所有子类实例共享父类引用类型。 和借用构造函数继承一样,每次创建对象都会创建一次方法。

寄生组合式继承

将寄生式继承和组合继承相结合,解决了组合式继承中会调用两次父类构造函数的缺点。

组合继承在第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors;它们都是 SuperType 的实例属性,只不过现在位于 SubType的原型中。当调用SubType构造函数时,又会调用一次SuperType构造函数,这一次又在新对象上创建了实例属性name和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

其背后的基本思路是:不必为了指定子类型的原型而调用父类的构造函数,我们需要的无非就是父类原型的一个副本而已。本质上,就是使用寄生式继承来继承父类的prototype,然后再将结果指定给子类的prototype。

function object(o) {
function F() { }
F.prototype = o;
return new F();
}

function inheritPrototype(SubType, SuperType) {
var prototype = object(SuperType.prototype); // 创建对象
prototype.constructor = SubType; // 增强对象
SubType.prototype = prototype; // 指定对象
}

// 父类
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
console.log(this.name);
};

// 子类
function SubType(name, age) {
// 继承父类实例属性
SuperType.call(this, name);

// 子类实例属性
this.age = age;
}

// 继承父类方法
inheritPrototype(SubType, SuperType);

// 子类方法
SubType.prototype.sayAge = function () {
console.log(this.age);
};

// 实例
var instance1 = new SubType('Tom', 20);
instance1.colors.push('black');
instance1.sayAge(); // 20
instance1.sayName(); // "Tom"
console.log(instance1.colors); // ["red", "blue", "green", "black"]

var instance2 = new SubType('Peter', 30);
instance2.sayAge(); // 30
instance2.sayName(); // "Peter"
console.log(instance2.colors); // ["red", "blue", "green"]

寄生组合式继承的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了再SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变。因此,还能够正常使用instanceof和isPrototypeOf()。

ES6实现继承

// 父类
class SuperType {
constructor(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}

sayName() {
console.log(this.name);
};
}

// 子类
class SubType extends SuperType {
constructor(name, age) {
// 继承父类实例属性和prototype上的方法
super(name);

// 子类实例属性
this.age = age;
}

// 子类方法
sayAge() {
console.log(this.age);
}
}

// 实例
var instance1 = new SubType('Tom', 20);
instance1.colors.push('black');
instance1.sayAge(); // 20
instance1.sayName(); // "Tom"
console.log(instance1.colors); // ["red", "blue", "green", "black"]

var instance2 = new SubType('Peter', 30);
instance2.sayAge(); // 30
instance2.sayName(); // "Peter"
console.log(instance2.colors); // ["red", "blue", "green"]

删除对象属性

如果删除成功,返回true,删除失败,返回false
var 声明的全局变量不能被删除

delete obj.name // 删除obj的name属性

// var 声明的全局变量不能被删除
var num = 12
str = 'hello'
delete window.num // false 删除失败
delete window.str // true 删除成功
console.log(num) // 12
console.log(str) // 报错 str is not undefined

判断一个属性是否是对象的一个属性

key in obj // 返回布尔值 从原型链继承的属性会返回 true

obj.hasOwnProperty(key) // 判断某个key是否是这个对象本身的属性

for..in 遍历对象

会枚举原型链中的属性

// 遍历对象
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key) // 键
console.log(obj[key]) // 值
}
}

Object.prototype 成员

constructor: 指向了构造函数 Object
hasOwnProperty(): 返回一个布尔值,判断对象自身是否具有该属性
isPrototypeOf(): 返回一个布尔值,用于测试一个对象是否存在于另一个对象的原型链上
propertyIsEnumerable(): 返回一个布尔值,表明指定的属性名是否是当前对象可枚举的自身属性
toString()/toLocaleString(): 返回对象的字符串格式
valueOf(): 返回对象的原始值

hasOwnProperty

var obj = { name: 'zs' }
// 判断name属性是不是obj自己提供的
obj.hasOwnProperty('name') // true
obj.hasOwnProperty('toString') // false

hasOwnPropertyin 的区别

  1. in 操作符:判断对象能否访问到该属性(不管这个属性是自己提供的,还是从原型上继承来的),如果可以访问到,都会返回 true

  2. hasOwnProperty:该属性必须是自己提供,才返回 true,否则返回 false

isPrototypeOf

// 判断 A 对象是否在 B 对象的原型链上 ,回一个布尔值
A.isPrototetypeOf(B)

function Person() {}
var p = new Person()

// p 的原型链:
// p ==> Person.prototype ==> Object.prototype ==> null

Person.isPrototypeOf(p) // false Person 是构造函数
Person.prototype.isPrototypeOf(p) // true
Object.prototype.isPrototypeOf(p) // true

isPropertyOfinstanceof 运算符的区别

instanceof 运算符用来测试一个对象的原型链中是否存在一个构造函数的 prototype 属性。作用和 isPrototypeOf 类似

语法: 实例对象 instanceof 构造函数

作用:构造函数的 prototype 属性是否在实例对象的原型链上

  • A.isPrototypeOf(B) 判断 A 是否在 B 的原型链上 A: 是一个原型对象
  • B instanceof A 判断 A 的 prototype 是否在 B 的原型链上 A:是一个构造函数
Array.isPrototypeOf([]) // false
Array.prototype.isPrototypeOf([]) // true

[] instanceof Array // true
[] instanceof Array.prototype // 语法错误

propertyIsEnumerable

function Person(name) {
this.name = name
}
Person.prototype.age = 19
var p = new Person('lw')

p.propertyIsEnumerable('name') // true
p.propertyIsEnumerable('age') // false

toString/toLocaleString

返回对象的字符串格式

每个内置对象的原型上都有属于自己的 toString 方法

var obj = { name: 'zs' }
// obj ==> Object.prototype ==> null
obj.toString() // '[object Object]'
obj.toLocaleString() // '[object Object]'

var arr = [1, 2, 3]
// arr ==> Array.prototype ==> Object.prototype ==> null
// toString() toString()
// Array.prototype 含有自己的 toString 方法
arr.toString() // '1,2,3'
arr.toLocaleString() // '1,2,3'

var date = new Date()
// date ==> Date.prototype ==> Object.prototype ==> null
// toString() toString()
// Date.prototype 含有自己的 toString 方法
date.toString() // Wed Oct 10 2018 16:00:51 GMT+0800 (中国标准时间)
date.toLocaleString() // 2018/10/10 下午4:00:51 得到的是本地时间格式

valueOf()

返回值为该对象的原始值,如果对象没有原始值,则 valueOf 将返回对象本身

对象 返回值
Array 返回数组对象本身
Boolean 布尔值
Date 时间戳
Function 函数本身
Number 数字值
Object 对象本身 (这是默认情况)
String 字符串值
var obj = { name: 'zs' }
// obj ==> Object.prototype ==> null
obj.valueOf() // { name: 'zs' }

var arr = [1, 2, 3]
// arr ==> Array.prototype ==> Object.prototype ==> null
arr.valueOf() // [1, 2, 3]

var date = new Date()
// date ==> Date.prototype ==> Object.prototype ==> null
// Date.prototype 含有 valueOf 方法
date.valueOf() // 时间戳

valueOf 和 toString 的应用

当对象在参与运算和比较的时候,js 内部会自动的调用 valueOf 和 toString 方法

调用规则:

  1. 默认先调用 vauleOf, 尝试将对象转成简单数据类型, 如果没有转成简单数据类型, 会继续在调用 toString 方法

  2. 如果 valueOf 和 toString 方法都没有转成简单数据类型,会报错

Object 静态方法

Object.assign()
Object.create()
Object.defineProperty()
Object.entries()
Object.freeze()
Object.getPrototypeOf()
Object.is()
Object.isFrozen()
Object.keys()
Object.values()

Object.assign(target, ...sources)

用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象

const o1 = { a: 1 }
const o2 = { b: 2 }
const o3 = { c: 3 }

const obj = Object.assign(o1, o2, o3)
console.log(obj) // { a: 1, b: 2, c: 3 }
console.log(o1) // { a: 1, b: 2, c: 3 }
obj === o1 // true
const v1 = "abc"
const v2 = true
const v3 = 10
const v4 = Symbol("foo")

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4)
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj) // { "0": "a", "1": "b", "2": "c" }

Object.create(proto, [propertiesObject])

创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

// 语法
Object.create(proto[, propertiesObject])
// 参数:proto 一个对象,新创建对象的原型对象
// 参数:propertiesObject 要添加到新创建对象的可枚举属性
// 返回值:一个新对象,带着指定的原型对象和属性

var obj = Object.create(proto)
console.log(obj)

Object.defineProperty(obj, prop, descriptor)

在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

let obj = {}
Object.defineProperty(obj, 'name', {
configurable: true, // 表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。默认为 false
value: 'c', // 配置该属性的默认值 默认为 undefined
writable: true, // 配置该属性是否可以被修改, 默认值是false, 不可修改
enumerable: true // 配置该属性是否可枚举, 默认值是false, 不可枚举
// 默认为 undefined
// set: function (newVal) {
// console.log('赋值了', newVal)
// },
// 默认为 undefined
// get: function () {
// console.log('取值了')
// }
})

如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常

Object.entries(obj)

返回一个给定对象自身可枚举属性的键值对数组

将Object转换为Map new Map(Object.entries(obj))

Object.freeze()

接受一个对象作为参数,并返回一个相同的不可变的对象,冻结一个对象后该对象的原型也不能被修改

可以阻止修改对象的值,但是不能阻止引用的修改

只是做了层浅冻结,具有嵌套属性的对象实际上并未冻结

Object.getPrototypeOf

返回指定对象的原型

Object.is()

Object.is() 方法判断两个值是否是相同的值。比较时不会做类型转换,这与 == === 运算符的判定方式都不一样

Object.is(+0, -0) // false
Object.is(0, -0) // false
Object.is(0, +0) // true
Object.is(-0, -0) // true

Object.is(undefined, undefined) // true
Object.is(null, null) // true
Object.is(Number.NaN, Number.NaN) // true
Object.is(Number.NaN, NaN) // true
Object.is(NaN, NaN) // true
Object.is(NaN, 0 / 0) // true

Object.isFrozen()

Object.keys(obj)

返回一个由一个给定对象的自身可枚举属性组成的数组

Object.values(obj)

返回一个给定对象自身的所有可枚举属性值的数组

Object.fromEntries

Array

new Array(4) // [empty × 4]
new Array('4') // ['4']

基本方法

arr.join()

将数组的值拼接成字符串 不传参数,默认用逗号进行拼接,返回拼接好的字符串

arr.push()

从后面添加一个或多个元素,多个参数逗号隔开,返回新数组的 length

arr.push(arr1) // 把 arr1 当成一个整体放到 arr 里

arr.pop()

从数组的后面删除元素,返回删除的那个元素

arr.unshift()

从数组的前面的添加元素,,多个参数逗号隔开,返回新数组的 length

arr.shift()

从数组的最前面删除元素,返回删除的那个元素

arr.reverse()

翻转数组

arr.sort()

排序

sort 方法可以传递一个函数作为参数,这个参数用来控制数组如何进行排序

arr.sort(function(a, b) {
// 参数为 true 时,即返回值 > 0 时,交换位置
// return a - b // 从小到大排序
return b - a // 从大到小排序
})

arr.concat()

数组合并,返回一个新数组,原数组不受影响

a.concat(b) // [...a, ...b]

arr.slice()

数组切分,复制数组的一部分到一个新数组,并返回这个新数组,原数组不受影响

// slice(begin, end) 包含 begin,不包含 end, begin 和 end 为下标
// slice(begin) 只有一个参数时,为开始参数,截取到末尾
// slice() 没有参数,全部截取
// 可以为负数,会将字符串的长度与对应的负数相加,结果作为参数,-1 表示从后数第一个
var newArr = arr.slice(begin, end)

arr.splice()

删除或者增加数组元素,修改原数组,返回删除的内容(数组形式)

// start: 开始位置  deletedCount: 删除的个数(如果不删除为 0)items: 替换的内容, 可为多个
arr.splice(start) // 删除原数组 start 位置之后的项(包含 start),返回删除的内容
arr.splice(start, deletedCount, [items]) // items 将作为 arr 的一项

arr.indexOf()

返回数组中某个元素第一次出现的位置,如果找不到,返回 -1

// fromIndex 表示从 fromIndex 下标开始查找
arr.indexOf('zs'[, fromIndex])

arr.lastIndexOf()

从后面开始查找数组中元素出现位置,即查找某元素最后一次出现的位置,如果找不到,返回 -1

arr.lastIndexOf('zs'[, fromIndex])

arr.forEach()

返回值: undefined
除了抛出异常以外,没有办法中止或跳出 forEach() 循环
不支持 return 操作输出,return 只用于控制循环是否跳出当前循环

遍历时会自动忽略 empty 值

arr.forEach(function(item, index, arr) {}, thisArg)
// item 必需。数组中正在处理的当前元素
// index 可选。数组中正在处理的当前元素的索引
// arr 可选。当前数组
// thisArg 可选。当执行回调函数时用作this的值
var arr = ['zs', 'ls', 'ww']
arr.forEach(function(item, index, arr) {
console.log(item)
console.log(this)
})

arr.map()

var newArr = arr.map(function(item, index) {
// item 必需。数组中正在处理的当前元素
// index 可选。数组中正在处理的当前元素的索引
// arr 可选。当前数组
// 使用 return 操作输出,会循环数组每一项,并返回新的每一项组成的数组
return item * 2
})
// 不修改原数组
// 返回一个新数组,新数组的每一项乘以 2

arr.filter()

var newArr = arr.filter(function(item, index) {
// 参数同 map
// 使用 return 操作输出,会循环数组每一项,并返回判断为 true 的每一项组成的数组
return item > 2 && item < 5 // return 后是判断条件
})
// 不修改原数组
// 返回一个新数组,新数组每一项满足 2 < item < 5

arr.some()

var newArr = arr.some(function(item, index) {
// 参数同 map
// 返回布尔值,只要有一项满足条件就返回 true,否则返回 false
return item > 2 // return 后是判断条件
})
// 不修改原数组

arr.every()

var newArr = arr.every(function(item, index) {
// 参数同 map
// 返回布尔值,只有所有项都满足条件才返回 true,否则返回f alse
return item > 2 // return 后是判断条件
})
// 不修改原数组

arr.includes()

判断数组是否含有某值,输出 true 或 false

var flag = arr.includes(5)

arr.find()

使用 return 操作输出,会循环数组每一项,当循环到满足条件时则跳出循环,输出当前数组元素
如果全不满足返回 undefined

var newArr = arr.find(function(item, index) {
return item > 2
})
// 不修改原数组

arr.findIndex()

使用 return 操作输出,会循环数组每一项,当循环到满足条件时则跳出循环,输出当前数组元素的下标
如果全不满足返回 -1

var index = arr.findIndex(function(item, index) {
return item > 2
})
// 不修改原数组

arr.reduce()

var new1 = arr.reduce(function(accumulator, current, index, array) {
// accumulator 第一次为数组第一项,之后为上一操作的结果
// current 数组的当前项
// index 当前项的序列
// array 可选。当前数组
// initialValue 可选。作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错
// 使用 return 操作输出
return accumulator + current // 返回数组每一项的和
}[, initialValue])
// 不修改原数组
// 扁平化数组
var arr2 = [ [1, 2, 3], [4, 5], [6, 7] ]
var new2 = arr2.reduce(function(accumulator, current, index) {
return accumulator.concat(current)
})
// 对象数组叠加计算
var arr3 = [
{ price: 1, count: 1 },
{ price: 2, count: 2 }
]
var new3 = arr3.reduce(function(accumulator, current, index) {
return accumulator + current.price * current.count

// 当需要操作第一项的时候,利用 reduce(callbreak(){},往数组第一项前添加一项,如:0)
}, 0) // 在原数组第一项添加为 0,不改变原数组

arr.fill()

用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引

arr.fill(value[, start[, end]])

arr.flat()

扁平化数组
按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

var newArray = arr.flat([depth])

// 使用 Infinity,可展开任意深度的嵌套数组
var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]
arr4.flat(Infinity) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// 会移除数组中的空项
var arr4 = [1, 2, , 4, 5]
arr4.flat() // [1, 2, 4, 5]

伪数组

伪数组也叫类数组

  1. 伪数组其实就是一个对象,但是跟数组一样,伪数组也会有length属性,也有0, 1, 2, 3等属性
  2. 伪数组并没有数组的方法,不能使用push/pop等方法
  3. 伪数组可以跟数组一样进行遍历,通过下标操作
  4. 常见的伪数组:argumentsdocument.getElementsByTagName的返回值jQuery对象
var obj = {
0: 'zs',
1: 'ls',
2: 'ww',
length: 3
}
  • 伪数组借用数组的方法
// 给 obj 添加一项
Array.prototype.push.call(obj, 'zl')
// 把 obj 中的每一项使用 '-' 拼接起来返回一个字符串
Array.prototype.join.call(obj, '-')
  • 将伪数组转换成真数组
// 借用数组的方法
Array.prototype.slice.call(obj)
[].slice.call(obj)

// 使用es6中数组的from方法:从一个类似数组或可迭代对象中创建一个新的数组实例
Array.from(obj)

// 对于函数的arguments参数可以使用扩展运算符
[...arguments]

String 对象

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String

操作字符串的方法不会改变原来的字符串,需要新字符串去接收

  • 查找指定字符串
str.indexOf(searchString[, position]) // 获取某个字符串第一次出现的位置,如果没有,返回-1。可选参数position可设置从str的某个指定的位置开始查找
str.lastIndexOf(searchString[, position]) // 从后面开始查找某个字符串第一次出现的位置。如果没有,返回-1

str.search(regexp) // 输出第一次出现位置,找不到输出-1
str.match(regexp) // 输出匹配到的第一个字符,匹配不到返回 null
str.match(regexp/g) // 全部输出

str.startsWith(str1) // 判断 str 字符串是否以 str1 字符串开头,若符合返回 true
// 等价于判断 str.indexOf(str1) === 0

str.endsWith(str1) // 判断 str 字符串是否以 str1 字符串结尾,若符合返回 true
// 等价于判断 str.indexOf(str1) === str.length - str1.length

str.includes(searchString[, position]) // 用于判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false
  • str.trim()

去除字符串两边的空格,内部空格不会去除

  • 大小写转换

str.toUpperCase() 全部转换成大写字母
str.toLowerCase() 全部转换成小写字母

  • 字符串拼接与截取

字符串拼接 可以用 concat,用法与数组一样,但是一般都用 +

str.slice(start, end)

从 start 开始,end 结束,并且取不到 end,没有 end 则截取从 start 到末尾
start 和 end 都不是必选,str.slice()str.slice(0) 等价 截取全部
start 和 end 可以是任意参数,参数应该是先调用Number(), 结果是NAN转换成0输出, 结果是整数直接输出, 结果是小数,再调用parseInt() 转化为整数输出
start 和 end 可以是负数,会将字符串的长度与对应的负数相加,结果作为参数,如果还是负数,不会递归继续与字符长度相加,取 0

str.substring(params1, params2)

以两个参数中较小一个作为起始位置,较大的参数作为结束位置,不包括结束位置;只有一个参数则截取到末尾
params1, params2 都不是必选,str.substring()str.substring(0) 等价,截取全部
可以是任意参数,参数应该是先调用Number(), 结果是NAN转换成0输出, 结果是整数直接输出, 结果是小数,再调用parseInt() 转化为整数输出
可以是负数,负参数会被直接转换为 0

str.substr(start, length)

从 start 开始,截取 length 个字符,没有 length 则截取到末尾
可以是任意参数,参数应该是先调用Number(), 结果是NAN转换成0输出, 结果是整数直接输出, 结果是小数,再调用parseInt() 转化为整数输出
start 参数为负参数时,会将参数与字符串长度相加后的结果作为参数,如果还是负数,不会递归继续与字符长度相加,取 0
length 参数为负数时,会被转化为 0 ,即截取长度为 0

  • 字符串切割
str.split(separator[, limit]) // 将字符串分割成数组

var str = 'zs,ls,ww'
var arr = str.split(',', 1) // ['zs']
var arr = str.split() // ['zs,ls,ww']
  • 字符串替换
// 语法
str.replace(regexp|substr, replacement)

// 参数:regexp/substr: 需要替换的内容 replacement: 替换文本或生成替换文本的函数 默认只替换第一个匹配子串
str.replace(/regexp/g, replacement) // 全部替换

str.replace(/ /g, '') // 将全部空格去掉

// replaceAll 全部替换

replace() 方法的参数 replacement 可以是函数。在这种情况下,每个匹配都调用该函数,它返回的字符串将作为替换文本使用。该函数的第一个参数是匹配模式的字符串。接下来的参数是与模式中的子表达式匹配的字符串,可以有 0 个或多个这样的参数。接下来的参数是一个整数,声明了匹配在 stringObject 中出现的位置。最后一个参数是 stringObject 本身。

  • str.charAt(pos)

pos参数可以是任意, 参数应该是先调用Number(), 结果是NAN转换成0输出,结果是整数直接输出,结果是小数,再调用 Math.ceil() 转化为整数输出
str.charAt() === str.charAt(0)

如果pos小于0或者大于等于字符串的长度str.length,返回空字符串

  • padEnd(maxLength, ?fillString)、padStart(maxLength, ?fillString)

padEnd() padStart() 方法用另一个字符串填充当前字符串(如果需要的话,会重复多次),以便产生的字符串达到给定的长度。padStart()从当前字符串的左侧开始填充,padEnd()从当前字符串的右侧开始填充

  • 寻找重复最多的字符以及个数
var str = 'shdshdfjkfjfdgjkjdksgjskdjfsfsfsfjksjkfdkjf'
var arr = str.split('').sort()
str = arr.join('')
var count = 0
var char = 0
var reg = /(\w)\1+/g
str.replace(reg, function(a, b, c, d) {
console.log(a, b, c, d) // a 匹配模式的字符串 b 与模式中的子表达式匹配的字符串 c 匹配在 str 中出现的位置 d str 本身
if (a.length > count) {
count = a.length
char = b
}
})
console.log(str)

console.log('最多的字符为:' + char + ';个数为:' + count)

安卓7.0以后,每个应用可以自定义可信 CA 集,默认情况下,应用只会信任系统级的证书,不再信任用户级的证书。通过Fiddler/Charles安装的证书属于用户级的证书,因此会被视作不安全的证书,导致无法正常抓包

解决方案

修改APP配置文件允许信任用户证书,但前提是可以获取到APP的源码

将证书安装到系统证书中,但需要root权限

安装好用户证书,系统已root,安装re文件管理器,使用re文件管理器将证书移动到系统证书文件夹

用户证书路径:/data/misc/user/0/cacerts-added/<证书Hash值>.编号
系统证书路径:/system/etc/security/cacerts

方法1: 使用adb连接

Copy
执行:

  1. adb push 0dd2455e.0 /sdcard
  2. adb shell
  3. su
  4. mount -o remount,rw /system

#mount -o rw,remount /system
5. cp /sdcard/0dd2455e.0 /system/etc/security/cacerts/
6. chmod 644 /system/etc/security/cacerts/0dd2455e.0

使用第三方文件管理器(推荐)

下载酷安市场app –> 分别搜索 Syslock 和 RE管理器 并下载安装(其他应用市场下载也可以)
给 Syslock 和 RE管理器 root权限(以小米为例:安全中心–>应用管理–>权限–>ROOT权限管理–>找到应用并开启)
打开Syslock并解锁 /system (每次重启后都得开启一次)
将文件拷贝至手机中
使用RE管理器将文件拷贝到 /system/etc/security/cacerts 下
修改文件权限
到此然后重启手机。就可以正常抓https数据包了。
也可以到手机 设置->安全->信任凭据–>系统 查看。

安卓模拟器(兼容性差,很多应用闪退)

安卓虚拟机 VMOS,官网: http://www.vmos.cn/,该方案相对简单

vmos 设置wifi代理

  1. 找到 vmos 主屏右下角的设置 — 其他设置 — 网络 adb (记录 ip+端口)
  2. 连接vmos: adb connect ip+端口
  3. 打开wlan设置: adb shell am start -a android.intent.action.MAIN -n com.android.settings/.wifi.WifiSettings
  4. 长按 wifi,修改网络,添加代理

typeof 操作符

是一个操作符而不是函数,圆括号可以使用,但不是必需的

var num
typeof num // undefined
typeof num1 // undefined
// 对未初始化和未声明的变量执行 typeof 操作符都返回 undefined 值

var num = 1
typeof num // number

var num = '1'
typeof num // string

var flag = true
typeof flag // bollean

typeof true // boolean

typeof null // object ==> null 被认为是一个空的对象引用

var cat = { name: 'kitty' }
typeof cat // object

算数操作符

+-*/%

对于加法 转换方向: 布尔值 -> 数值 -> 字符串, 数组/对象 -> 字符串
除加法以外 转换方向:字符串 -> 数值,布尔值 -> 数值, 数组 -> 数值

var num = 5 + 6 // 11
var num = 5 % 2 // 1 取余数
var num = 5 % -2 // 1
var num = -5 % 2 // -1 只与左边值的符号有关

// 数字与字符串相加,数字会转换成字符串,返回字符串
var num = '5' + 6 + 7 // '567'
var num = 5 + 6 + '7' // '117'

// 数值与布尔值相加,布尔值转化成数值,false 转成 0,true 转成 1
var num = 5 + true // 6

// 字符串与布尔值相加,布尔值转化成字符串
var num = '1' + true // '1true'

'a' + +'b' // -> 'a' + +'b' -> 'aNaN'
4 * '3' // 12
true + true // 2

1 / 0 // Infinity
0 / 0 // NaN

赋值操作符

赋值运算符左边不能是常量或表达式

var age = 10
var num = age++ // num = 10 age = 11 (先将变量中的值取出做赋值操作,再自身+1)

var age = 10
var num = ++age // num = 11 age = 11 (先自身+1,然后再将+1后的结果赋值)

var num = 5
console.log(num++) // 5
console.log(++num) // 7

var x = 3
var y = x++ + ++x + x * 10 // x = 3
// y = 3 + ++x + x * 10 // x = 4
// y = 3 + 5 + x * 10 // x = 5
// y = 58

关系操作符

><>=<=

== 相等 、!= 不相等 、=== 全等 、!== 不全等

in instanceof

  • ><>=<===!=

在比较前先执行类型转换

  • 如果有一个操作数是布尔值,则在比较相等性前先将其转换为数值 -> false 转换为 0,true 转换为 1
  • 如果一个操作数是字符串,另一个操作数是数值,则在比较相等性前将字符串转换为数值
  • 如果两个值都是字符串,则按照字符串的字符编码进行逐位比较
  • 如果一个数是对象,另一个不是,则调用对象的 valueOf()方法,用得到的基本类型值按照前面的规则比较
  • 如果两个操作数都是对象,则比较它们是否指向同一个对象
  • null 和 undefined 是相等的
  • 在比较相等性之前,不能将 null 和 undefined 转换为其他任何值
  • 在比较大小之前,null,undefined 会被 Number()强制转换成数字类型 Number(null) -> 0, Number(undefined) -> NaN
  • 如果有一个操作符是 NaN,则相等操作符返回 false,不相等操作符返回 true;即使两个操作数都是 NaN,也一样
  • === 全等、!== 不全等
    两个操作数在未经转换的情况下相等返回 true,不相等返回 false
0 == false // true
1 == true // true
2 == true // false

false == '0' // true
'' == 0 // true
'4' == 4 // true

null == undefined // true
undefined == 0 // false

null >= 0 // true
null <= 0 // true
null == 0 // false

null >= false // true
null <= false // true
null == false // false

'NaN' == NaN // false
5 == NaN // false
NaN == NaN // false
NaN != NaN // true

[] != [] // true
[] == ![] // true
0 == [] // true
1 == [] // false
1 == [1] // true

0 + [] // '0'
0 + {} // "0[object Object]"

{} + [] === 0 // true {} 被解析为空的 block, + 被解析为正号运算符,结果等于 +[]
[] + {} === "[object Object]" // true [] 被解析为数组,后续的 + 被解析为加法运算符
({} + []) // '[object Object] 括号会阻止js将{}识别为block,因此他的运算结果与 []+{} 一致
console.log({} + []) // '[object Object] 当表达式作为参数传递给函数时,不会被默认为新的block
console.log({} + [] === 0) // false

[] + {} === {} + [] // true

-0 === 0 // true
'4' === 4 // false
undefined === null // false
  • in

    判断对象是否能够访问到该属性

  • instanceof

    判断一个对象是否是另一个对象的实例

逻辑操作符

! 非、&& 与、||

  • ! 对 Boolean 值取反
var flag = true
alert(!flag) // false

alert(!0) // true
alert(![]) // false
alert(!'') // true
alert(!![]) // true
alert(!!1) // true
  • && 如果第一个值转换成 boolean 值之后为 true, 则输出第二个值;如果第一个值转换成 boolean 值之后为 false,则输出第一个值,且第二个值不在执行。(取第一个为 false 的值,如果都为 true ,则输出最后一个值。)
var result = true && 3 // 3
var result = 1 && 3 // 3
var result = [] && '' // ''
var result = false && 3 // false
var result = '' && 3 // ''
var result = null && true // null

var num = 0
var result = '' && num++ // '' num = 0

&& 使用场景

function animate(fn) {
fn && fn()
}
// 不传参数不会报错
animate()
  • || 如果第一个值转换成 boolean 值之后为 true, 则输出第一个值,且第二个值不在执行;如果第一个值转换成 boolean 值之后为 false,则输出第二个值,以此类推,(取第一个为 true 的值,如果都为 false ,则输出最后一个值。)
var result = true || 3 // true
var result = 1 || 3 // 1
var result = [] || '' // []
var result = false || 0 // 0
var result = '' || 3 // 3
var result = num || true // true

var num = 0
var result = 3 || num++ // 3 num=0

|| 使用场景

// 1.兼容性问题:
var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop

// 2. 函数的参数默认值
function sum(n) {
n = n || 10 // 给形参 n 设置默认值
console.log(n + 10)
}
sum()

操作符的优先级

从高到低如下:

  • () 优先级最高

  • 一元运算符 ++ – !

  • 算数运算符 先 * / % 后 + -

  • 关系运算符 > >= < <=

  • 相等运算符 == != === !==

  • 逻辑运算符 先 && 后 ||

  • 赋值运算符 =

空值合并操作符 ??

注意与或操作符区别

var a = b ?? c

// 当且仅当 b 为 null 或 undefined 时,返回 c,否则返回 b

// 等价于
var a
if ( b === null || b === undefined ){
a = c
} else {
a = b
}

可选链操作符 ?.

在引用为空(nullish) (null 或者 undefined) 的情况下不会引起错误,会短路返回值,返回 undefined

// 访问属性、调用方法
obj?.customMethod?.()

// 数组取值
arr?.[5]

// 短路计算
let obj1 = null
let a = 0
let prop1 = obj1?.[a++] // a => 0

let obj2 = 0
let b = 0
let prop2 = obj2?.[b++] // b => 1

转换成字符串

  • 调用 toString() 方法 (显式转换)
var a = 1
a.toString() // '1'

null 、undefined 没有 toString() 方法

  • 调用 String() 构造函数(显式转换)
String(1) // '1'
  • 直接和字符串做加法运算(隐式转换)
// 任意数据类型的变量和字符串做加法运算结果都是字符串
1 + '' // '1'
true + '' // 'true'
  • 引用类型转换成字符串
[].toString() // ''
[1, 2].toString() // '1,2'
({}).toString() // "[object Object]"

[1, '123', [], undefined, null, NaN, true ].toString() // "1,123,,,,NaN,true"

转换成数值

  • Number()
Number('123') // 123
Number('123c') // NaN
// 如果字符串不能转换成合法数字,转换结果为 NaN
  • 使用 parseInt()
parseInt('12.3') // 12 只保留整数

parseInt('15xyz') // 15
parseInt('15x6yz') // 15
// 如果字符串里有非法的数字,会逐个转换,直到遇到无法转化的字符串为止
  • 使用 parseFloat() 完成
parseFloat('12.34') // 12.34 可以保留小数位
  • 让字符串和数字做除了加法以外的运算(隐式转换)
var d = '345'
+d === 345 // true
d - 0 === 345 // true
d * 1 === 345 // true
d / 1 === 345 // true
  • 引用类型转换成数值
// 空数组为 0,存在一个元素且可转换成数字,则转换成该数字,其他情况为 NaN
+[] // 0
+['2'] // 2
+['1', '2'] // NaN
+{} // NaN

转换成布尔值

0, NaN, 空字符串,undefined, null, false 会被转换成为 false

  • 使用 Boolean() 完成
Boolean(123) // true
// 对于数字类型来说:一般的数字都转换成为 true,0、NaN 会被转换成为 false

Boolean('123') // true
Boolean(' ') // true
Boolean('') // false
// 字符串中只有空的字符串会被转换成为 false

Boolean(undefined) // false
Boolean(null) // false
  • 使用 !!
!!2 // true
  • 自动转换
if ('') { console.log('哈哈') }

变量转换表

Value Boolean Number String
undefined false NaN “undefined”
null false 0 “null”
true true 1 “true”
false false 0 “false”
“” false 0 “”
“123” true 123 “123”
“1a” true NaN “1a”
0 false 0 “0”
1 true 1 “1”
Infinity true Infinity “Infinity”
NaN false NaN “NaN”
{} true NaN “[object Object]”
数组 true 空数组为 0,存在一个元素且为数字转数字,其他 NaN [1, ‘123’, [], undefined, null, NaN, true ] => “1,123,,,,NaN,true”

相关链接

v2ray 用户手册 github
V2Ray 白话文指南 github
在线配置生成
v2ray web面板 v2-ui
SSPanel-Uim 节点管理面板/用户管理系统
使用宝塔部署 SSPanel 魔改版

安装

一键脚本安装

bash <(curl -s -L https://git.io/v2ray.sh)

安装完成后,输入 v2ray 即可管理 V2Ray

管理命令

v2ray info # 查看 V2Ray 配置信息
v2ray config # 修改 V2Ray 配置
v2ray link # 生成 V2Ray 配置文件链接
v2ray infolink # 生成 V2Ray 配置信息链接
v2ray qr # 生成 V2Ray 配置二维码链接
v2ray ss # 修改 Shadowsocks 配置
v2ray ssinfo # 查看 Shadowsocks 配置信息
v2ray ssqr # 生成 Shadowsocks 配置二维码链接
v2ray status # 查看 V2Ray 运行状态
v2ray start # 启动 V2Ray
v2ray stop # 停止 V2Ray
v2ray restart # 重启 V2Ray
v2ray log # 查看 V2Ray 运行日志
v2ray update # 更新 V2Ray
v2ray update.sh # 更新 V2Ray 管理脚本
v2ray uninstall # 卸载 V2Ray

官方脚本

官方脚本

# 安装
bash <(curl -L -s https://install.direct/go.sh)

# 安装完成后记住 PORT UUID,忘记也没关系可以使用下面命令查看

# 启动
systemctl start v2ray

# 查看端口 Port
cat /etc/v2ray/config.json | grep port

# 查看 id (UUID)
cat /etc/v2ray/config.json | grep id

卸载

如果脚本不支持卸载,可使用以下方法手动卸载

其中 systemd 和 sysv 二选一,取决于你的系统

#停用并卸载服务(systemd)
systemctl stop v2ray
systemctl disable v2ray

#停用并卸载服务(sysv)
service v2ray stop
update-rc.d -f v2ray remove

# 删除文件
# 配置文件
rm -rf /etc/v2ray/*
# 程序
rm -rf /usr/bin/v2ray/*
# 日志
rm -rf /var/log/v2ray/*
# systemd 启动项
rm -rf /lib/systemd/system/v2ray.service
# sysv 启动项
rm -rf /etc/init.d/v2ray

Nginx + WebSocket + TLS

参考:https://guide.v2fly.org/advanced/wss_and_web.html

V2Ray 脚本可直接使用 Caddy 配置 WebSocket + TLS 传输协议,但是如果想在 vps 上同时使用 nginx 跑一个小博客,那么会导致 caddy 和 nginx 监听端口时发生冲突,这显然不是我们想要的

所以就要将 TLS 部分放到 nginx 程序里面去实现

首先使用官方脚本安装好服务端程序

申请 SSL 证书

如果使用宝塔面板,可以通过面板一键生成 ssl 证书,快速配置 nginx+ssl

申请免费 SSL 证书参考这里

nginx 配置

如果使用宝塔面板,可以通过面板一键生成 ssl 证书,快速配置 nginx+ssl

server {
listen 443 ssl;
listen [::]:443 ssl;
server_name aaa.com; # 域名

# 配置ssl
ssl_certificate /etc/letsencrypt/aaa.com.crt; # 证书
ssl_certificate_key /etc/letsencrypt/aaa.com.key; # 密钥
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
#

location /ray { # 与 V2Ray 服务端配置中的 path 保持一致
if ($http_upgrade != "websocket") { # WebSocket协商失败时返回404
return 404;
}
proxy_redirect off;
proxy_pass http://127.0.0.1:2333; # 假设 WebSocket 监听在环回地址的 2333 端口上
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# Show real IP in v2ray access.log
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

v2ray 服务端配置

/etc/v2ray/config.json

{
"inbounds": [
{
"port": 2333, // WebSocket 监听端口
"listen": "127.0.0.1", // 只监听 127.0.0.1,避免除本机外的机器探测到开放了 22445 端口
"protocol": "vmess",
"settings": {
"clients": [
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx",
"alterId": 32
}
]
},
"streamSettings": {
"network": "ws",
"wsSettings": {
"path": "/ray"
}
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
}
]
}

客户端配置

{
"inbounds": [
{
"port": 1080,
"listen": "127.0.0.1",
"protocol": "socks",
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls"]
},
"settings": {
"auth": "noauth",
"udp": false
}
}
],
"outbounds": [
{
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "aaa.com", // nginx server_name
"port": 443,
"users": [
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", // 同服务端配合
"alterId": 32 // 同服务端配合
}
]
}
]
},
"streamSettings": {
"network": "ws",
"security": "tls",
"wsSettings": {
"path": "/ray" // 与 V2Ray 服务端配置中的 path 保持一致
}
}
}
]
}

Nginx+vmess+ws+tls/ http2 over tls 一键安装脚本

https://github.com/wulabing/V2Ray_ws-tls_bash_onekey

证书签发错误的原因是签发服务器已经不认默认的邮箱名:@example.com,

- curl https://get.acme.sh | sh
+ curl https://get.acme.sh | sh -s email=admin@youremail.com

添加-s 参数, 把你的邮箱放进去

另外如果脚本全部正常跑完, 还是不能用的,请用这个网站 https://www.matools.com/port 检查下你的端口是不是已经被封了

v2ray 配置好后无法连接解决办法

# 检查端口占用情况
yum install net-tools
netstat -apn | grep v2ray

# 发现v2ray并没有监听我们的公网IP,只监听了一个IPV6:
# tcp6 0 0 :::40682 :::* LISTEN 19553/v2ray
# unix 3 [ ] STREAM CONNECTED 80938 19553/v2ray

# 修改配置文件添加 listen 字段

# v2ray默认配置文件在/etc/v2ray/conf.json
"inbound": {
"listen":"12.34.56.78",
}

# v2ray 测试配置文件是否正确
/usr/bin/v2ray/v2ray --test --config /etc/v2ray/config.json

客户端

客户端可以参考这里