🎄 Предисловие
В этой статье в основном обобщаются рукописные вопросы, которые будут заранее одобрены внешним отделом в 2021 году и проверены Осенним рекрутинговым институтом. с начала марта до конца октября.Фейсбуки приходят от Ali, Tencent, Baidu, Byte, Meituan, Jingdong, Kuaishou, Pinduoduo и других 15 компаний и сделали простое частотное разделение.
- ⭐⭐⭐⭐⭐: Принял участие в более чем 10 интервью в 15 компаниях.
- ⭐⭐⭐⭐: Появляется с 5 по 10 в 15 интервью по формуле.
- ⭐⭐⭐: 3-5 место в 15 интервью компании
- Нет звезд: Появляется 1-2
Часть анализа темы исходит от написания небольшого пакета, а другая часть, если я чувствую, что тема лучше расширена, я выберу некоторые ссылки на блоги больших парней.
🌟 обещаю
выполнять обещания
Частота осмотра: (⭐⭐⭐⭐⭐)
реализовать обещание.все
Частота осмотра: (⭐⭐⭐⭐⭐)
function PromiseAll(promises){
return new Promise((resolve, reject)=>{
if(!Array.isArray(promises)){
throw new TypeError("promises must be an array")
}
let result = []
let count = 0
promises.forEach((promise, index) => {
promise.then((res)=>{
result[index] = res
count++
count === promises.length && resolve(result)
}, (err)=>{
reject(err)
})
})
})
}
реализовать обещание.наконец-то
Частота осмотра: (⭐⭐⭐⭐⭐)
Promise.prototype.finally = function (cb) {
return this.then(function (value) {
return Promise.resolve(cb()).then(function () {
return value
})
}, function (err) {
return Promise.resolve(cb()).then(function () {
throw err
})
})
}
Реализовать обещание.allSettled
Частота осмотра: (⭐⭐⭐⭐)
function allSettled(promises) {
if (promises.length === 0) return Promise.resolve([])
const _promises = promises.map(
item => item instanceof Promise ? item : Promise.resolve(item)
)
return new Promise((resolve, reject) => {
const result = []
let unSettledPromiseCount = _promises.length
_promises.forEach((promise, index) => {
promise.then((value) => {
result[index] = {
status: 'fulfilled',
value
}
unSettledPromiseCount -= 1
// resolve after all are settled
if (unSettledPromiseCount === 0) {
resolve(result)
}
}, (reason) => {
result[index] = {
status: 'rejected',
reason
}
unSettledPromiseCount -= 1
// resolve after all are settled
if (unSettledPromiseCount === 0) {
resolve(result)
}
})
})
})
}
Реализовать обещание.race
Частота осмотра: (⭐⭐⭐)
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
rejecte(err)
})
})
})
}
Давайте поговорим о том, как последовательно выполнять несколько промисов.
promise.any
Promise.any = function(promiseArr) {
let index = 0
return new Promise((resolve, reject) => {
if (promiseArr.length === 0) return
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
index++
if (index === promiseArr.length) {
reject(new AggregateError('All promises were rejected'))
}
})
})
})
}
resolve
Promise.resolve = function(value) {
if(value instanceof Promise){
return value
}
return new Promise(resolve => resolve(value))
}
reject
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason))
}
🐳 Массив
Дедупликация массива
Частота осмотра: (⭐⭐⭐⭐⭐)
использовать двойнойfor
иsplice
function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++){
if(arr[i]==arr[j]){
//第一个等同于第二个,splice方法删除第二个
arr.splice(j,1);
// 删除后注意回调j
j--;
}
}
}
return arr;
}
использоватьindexOf
илиincludes
добавить новый массив
//使用indexof
function unique(arr) {
var uniqueArr = []; // 新数组
for (let i = 0; i < arr.length; i++) {
if (uniqueArr.indexOf(arr[i]) === -1) {
//indexof返回-1表示在新数组中不存在该元素
uniqueArr.push(arr[i])//是新数组里没有的元素就push入
}
}
return uniqueArr;
}
// 使用includes
function unique(arr) {
var uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
//includes 检测数组是否有某个值
if (!uniqueArr.includes(arr[i])) {
uniqueArr.push(arr[i])//
}
}
return uniqueArr;
}
sort
После сортировки появилась идея использования быстрых и медленных указателей
function unique(arr) {
arr.sort((a, b) => a - b);
var slow = 1,
fast = 1;
while (fast < arr.length) {
if (arr[fast] != arr[fast - 1]) {
arr[slow ++] = arr[fast];
}
++ fast;
}
arr.length = slow;
return arr;
}
sort
Метод используется для сортировки от меньшего к большему (возвращает новый массив).Если указанная выше callback-функция не включена в его параметры, то будет ошибка сортировки при наличии двух и более цифр (если опустить, то элементы будут преобразованы в соответствии с символами каждого символа преобразованной строки).Unicode
Сайты отсортированы. Двузначное число становится строкой длины два для вычисления).
ES6
который предоставилSet
дедупликация
function unique(arr) {
const result = new Set(arr);
return [...result];
//使用扩展运算符将Set数据结构转为数组
}
Set
Элементы будут появляться только один раз, т.е.Set
Элементы уникальны.
Используйте хеш-таблицу для хранения того, встречается ли элемент (ES6
который предоставилmap
)
function unique(arr) {
let map = new Map();
let uniqueArr = new Array(); // 数组用于返回结果
for (let i = 0; i < arr.length; i++) {
if(map.has(arr[i])) { // 如果有该key值
map.set(arr[i], true);
} else {
map.set(arr[i], false); // 如果没有该key值
uniqueArr.push(arr[i]);
}
}
return uniqueArr ;
}
map
Объекты содержат пары ключ-значение, как и объекты. ноmap
Ключи объекта могут быть любого типа, а ключи объекта могут быть только строкового типа.
Также можно использовать простые объекты в качестве хеш-таблиц, если в массиве только числа.
filter
СотрудничатьindexOf
function unique(arr) {
return arr.filter(function (item, index, arr) {
//当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
//不是那么就证明是重复项,就舍弃
return arr.indexOf(item) === index;
})
}
Здесь могут быть сомнения, приведу пример:
const arr = [1,1,2,1,3]
arr.indexOf(arr[0]) === 0 // 1 的第一次出现
arr.indexOf(arr[1]) !== 1 // 说明前面曾经出现过1
reduce
Сотрудничатьincludes
function unique(arr){
let uniqueArr = arr.reduce((acc,cur)=>{
if(!acc.includes(cur)){
acc.push(cur);
}
return acc;
},[]) // []作为回调函数的第一个参数的初始值
return uniqueArr
}
Выравнивание массива
Частота осмотра: (⭐⭐⭐)
forEach
Частота осмотра: (⭐⭐⭐)
Array.prototype.myForEach = function (callbackFn) {
// 判断this是否合法
if (this === null || this === undefined) {
throw new TypeError("Cannot read property 'myForEach' of null");
}
// 判断callbackFn是否合法
if (Object.prototype.toString.call(callbackFn) !== "[object Function]") {
throw new TypeError(callbackFn + ' is not a function')
}
// 取到执行方法的数组对象和传入的this对象
var _arr = this, thisArg = arguments[1] || window;
for (var i = 0; i < _arr.length; i++) {
// 执行回调函数
callbackFn.call(thisArg, _arr[i], i, _arr);
}
}
reduce
Частота осмотра: (⭐⭐⭐)
Array.prototype.myReduce = function(callbackFn) {
var _arr = this, accumulator = arguments[1];
var i = 0;
// 判断是否传入初始值
if (accumulator === undefined) {
// 没有初始值的空数组调用reduce会报错
if (_arr.length === 0) {
throw new Error('initVal and Array.length>0 need one')
}
// 初始值赋值为数组第一个元素
accumulator = _arr[i];
i++;
}
for (; i<_arr.length; i++) {
// 计算结果赋值给初始值
accumulator = callbackFn(accumulator, _arr[i], i, _arr)
}
return accumulator;
}
map
Array.prototype.myMap = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window, res = [];
for (var i = 0; i<_arr.length; i++) {
// 存储运算结果
res.push(callbackFn.call(thisArg, _arr[i], i, _arr));
}
return res;
}
filter
Array.prototype.myFilter = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window, res = [];
for (var i = 0; i<_arr.length; i++) {
// 回调函数执行为true
if (callbackFn.call(thisArg, _arr[i], i, _arr)) {
res.push(_arr[i]);
}
}
return res;
}
every
Array.prototype.myEvery = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window;
// 开始标识值为true
// 遇到回调返回false,直接返回false
// 如果循环执行完毕,意味着所有回调返回值为true,最终结果为true
var flag = true;
for (var i = 0; i<_arr.length; i++) {
// 回调函数执行为false,函数中断
if (!callbackFn.call(thisArg, _arr[i], i, _arr)) {
return false;
}
}
return flag;
}
some
Array.prototype.mySome = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window;
// 开始标识值为false
// 遇到回调返回true,直接返回true
// 如果循环执行完毕,意味着所有回调返回值为false,最终结果为false
var flag = false;
for (var i = 0; i<_arr.length; i++) {
// 回调函数执行为false,函数中断
if (callbackFn.call(thisArg, _arr[i], i, _arr)) {
return true;
}
}
return flag;
}
find/findIndex
Array.prototype.myFind = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window;
// 遇到回调返回true,直接返回该数组元素
// 如果循环执行完毕,意味着所有回调返回值为false,最终结果为undefined
for (var i = 0; i<_arr.length; i++) {
// 回调函数执行为false,函数中断
if (callbackFn.call(thisArg, _arr[i], i, _arr)) {
return _arr[i];
}
}
return undefined;
}
indexOf
function indexOf(findVal, beginIndex = 0) {
if (this.length < 1 || beginIndex > findVal.length) {
return -1;
}
if (!findVal) {
return 0;
}
beginIndex = beginIndex <= 0 ? 0 : beginIndex;
for (let i = beginIndex; i < this.length; i++) {
if (this[i] == findVal) return i;
}
return -1;
}
реализовать сортировку
🌊 Дроссельная защита от сотрясений
Реализовать функцию устранения дребезга
Частота осмотра: (⭐⭐⭐⭐⭐)
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
result = func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
Реализовать функцию дроссельной заслонки
Частота осмотра: (⭐⭐⭐⭐⭐)
// 第四版
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
⛲ Объекты
Можно ли написать полную глубокую копию
Частота осмотра: (⭐⭐⭐⭐⭐)
const getType = obj => Object.prototype.toString.call(obj);
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const canTraverse = {
'[object Map]': true,
'[object Set]': true,
'[object Array]': true,
'[object Object]': true,
'[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const handleRegExp = (target) => {
const { source, flags } = target;
return new target.constructor(source, flags);
}
const handleFunc = (func) => {
// 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}
const handleNotTraverse = (target, tag) => {
const Ctor = target.constructor;
switch(tag) {
case boolTag:
return new Object(Boolean.prototype.valueOf.call(target));
case numberTag:
return new Object(Number.prototype.valueOf.call(target));
case stringTag:
return new Object(String.prototype.valueOf.call(target));
case symbolTag:
return new Object(Symbol.prototype.valueOf.call(target));
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
}
}
const deepClone = (target, map = new WeakMap()) => {
if(!isObject(target))
return target;
let type = getType(target);
let cloneTarget;
if(!canTraverse[type]) {
// 处理不能遍历的对象
return handleNotTraverse(target, type);
}else {
// 这波操作相当关键,可以保证对象的原型不丢失!
let ctor = target.constructor;
cloneTarget = new ctor();
}
if(map.get(target))
return target;
map.set(target, true);
if(type === mapTag) {
//处理Map
target.forEach((item, key) => {
cloneTarget.set(deepClone(key, map), deepClone(item, map));
})
}
if(type === setTag) {
//处理Set
target.forEach(item => {
cloneTarget.add(deepClone(item, map));
})
}
// 处理数组和对象
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
}
внедрять новые
Частота осмотра: (⭐⭐⭐⭐)
function createObject(Con) {
// 创建新对象obj
// var obj = {};也可以
var obj = Object.create(null);
// 将obj.__proto__ -> 构造函数原型
// (不推荐)obj.__proto__ = Con.prototype
Object.setPrototypeOf(obj, Con.prototype);
// 执行构造函数,并接受构造函数返回值
const ret = Con.apply(obj, [].slice.call(arguments, 1));
// 若构造函数返回值为对象,直接返回该对象
// 否则返回obj
return typeof(ret) === 'object' ? ret: obj;
}
наследовать
Частота осмотра: (⭐⭐⭐⭐)
Наследование цепочки прототипов
Заимствованные конструкторы (классическое наследование)
наследование композиции
прототипное наследование
паразитарное наследование
Паразитическая композиционная наследственность
Класс реализует наследование (дополнительно)
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
реализовать object.create
function newCreate(proto, propertiesObject) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw TypeError('Object prototype may only be an Object: ' + proto)
}
function F() { }
F.prototype = proto
const o = new F()
if (propertiesObject !== undefined) {
Object.keys(propertiesObject).forEach(prop => {
let desc = propertiesObject[prop]
if (typeof desc !== 'object' || desc === null) {
throw TypeError('Object prorotype may only be an Object: ' + desc)
} else {
Object.defineProperty(o, prop, desc)
}
})
}
return o
}
🚂 Функция
call
Частота осмотра: (⭐⭐⭐⭐)
Function.prototype.myCall = function (thisArg) {
thisArg = thisArg || window;
thisArg.func = this;
const args = []
for (let i = 1; i<arguments.length; i++) {
args.push('arguments['+ i + ']')
}
const result = eval('thisArg.func(' + args +')')
delete thisArg.func;
return result;
}
bind
Частота осмотра: (⭐⭐⭐⭐)
Function.prototype.sx_bind = function (obj, ...args) {
obj = obj || window
const fn = Symbol()
obj[fn] = this
const _this = this
const res = function (...innerArgs) {
console.log(this, _this)
if (this instanceof _this) {
this[fn] = _this
this[fn](...[...args, ...innerArgs])
delete this[fn]
} else {
obj[fn](...[...args, ...innerArgs])
delete obj[fn]
}
}
res.prototype = Object.create(this.prototype)
return res
}
apply
Частота осмотра: (⭐⭐⭐⭐)
Function.prototype.myApply = function (thisArg, arr) {
thisArg = thisArg || window;
thisArg.func = this;
const args = []
for (let i = 0; i<arr.length; i++) {
args.push('arr['+ i + ']')
}
const result = eval('thisArg.func(' + args +')')
delete thisArg.func;
return result;
}
Реализовать каррирование
Частота осмотра: (⭐⭐⭐)
Реализовать связанные вызовы
частичная функция
🌍 аякс и jsonp
Частота осмотра: (⭐⭐⭐)
внедрить аякс
function ajax({
url= null,
method = 'GET',
dataType = 'JSON',
async = true}){
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open(method, url, async)
xhr.responseType = dataType
xhr.onreadystatechange = () => {
if(!/^[23]\d{2}$/.test(xhr.status)) return;
if(xhr.readyState === 4) {
let result = xhr.responseText
resolve(result)
}
}
xhr.onerror = (err) => {
reject(err)
}
xhr.send()
})
}
реализовать jsonp
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = ''
for (let key in params) {
if (params.hasOwnProperty(key)) {
dataSrc += `${key}=${params[key]}&`
}
}
dataSrc += `callback=${callbackName}`
return `${url}?${dataSrc}`
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script')
scriptEle.src = generateUrl()
document.body.appendChild(scriptEle)
window[callbackName] = data => {
resolve(data)
document.removeChild(scriptEle)
}
})
}
🛫 статьи ES6
набор инструментов
class Set {
constructor() {
this.items = {};
this.size = 0;
}
has(element) {
return element in this.items;
}
add(element) {
if(! this.has(element)) {
this.items[element] = element;
this.size++;
}
return this;
}
delete(element) {
if (this.has(element)) {
delete this.items[element];
this.size--;
}
return this;
}
clear() {
this.items = {}
this.size = 0;
}
values() {
let values = [];
for(let key in this.items) {
if(this.items.hasOwnProperty(key)) {
values.push(key);
}
}
return values;
}
}
реализовать карту
function defaultToString(key) {
if(key === null) {
return 'NULL';
} else if (key === undefined) {
return 'UNDEFINED'
} else if (Object.prototype.toString.call(key) === '[object Object]' || Object.prototype.toString.call(key) === '[object Array]') {
return JSON.stringify(key);
}
return key.toString();
}
class Map {
constructor() {
this.items = {};
this.size = 0;
}
set(key, value) {
if(!this.has(key)) {
this.items[defaultToString(key)] = value;
this.size++;
}
return this;
}
get(key) {
return this.items[defaultToString(key)];
}
has(key) {
return this.items[defaultToString(key)] !== undefined;
}
delete(key) {
if (this.has(key)) {
delete this.items[key];
this.size--;
}
return this;
}
clear() {
this.items = {}
this.size = 0;
}
keys() {
let keys = [];
for(let key in this.items) {
if(this.has(key)) {
keys.push(key)
}
}
return keys;
}
values() {
let values = [];
for(let key in this.items) {
if(this.has(key)) {
values.push(this.items[key]);
}
}
return values;
}
}
Класс, реализующий es6
🦉 другое
instanceof
Частота осмотра: (⭐⭐⭐⭐)
function instance_of(Case, Constructor) {
// 基本数据类型返回false
// 兼容一下函数对象
if ((typeof(Case) != 'object' && typeof(Case) != 'function') || Case == 'null') return false;
let CaseProto = Object.getPrototypeOf(Case);
while (true) {
// 查到原型链顶端,仍未查到,返回false
if (CaseProto == null) return false;
// 找到相同的原型
if (CaseProto === Constructor.prototype) return true;
CaseProto = Object.getPrototypeOf(CaseProto);
}
}
Реализовать разделитель тысяч
Частота осмотра: (⭐⭐⭐)
var str = "100000000000",
reg = /(?=(\B\d{3})+$)/g;
str.replace(reg, ",")
Преобразование ключа объекта JSON из символа подчеркивания (Pascal) в camelCase (верблюд)
Частота осмотра: (⭐⭐⭐)
Реализовать функцию оценки типа данных
function myTypeof(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}
Реализовать массив в дереве
Реализовать функцию сна
// promise
const sleep = time => {
return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
console.log(1)
})
// ES5
function sleep(callback,time) {
if(typeof callback === 'function')
setTimeout(callback,time)
}
function output(){
console.log(1);
}
sleep(output,1000);
Реализовать модель публикации-подписки
class EventEmitter {
constructor() {
this.cache = {}
}
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
}
if (once) {
delete this.cache[name]
}
}
}
}
🛕 Больше тем
Портал:Передняя тема
Не забудьте поставить пакету ⭐, если вы считаете его полезным.
рекомендуемая статья
Недавние события
- Используйте анимацию частиц, чтобы запомнить время весеннего фестиваля | Поддержите эмодзи
- Вращай, реагируй! - Танцующие иконки React
- Супердекомпрессия, реализующая универсальные гравитационные и отталкивающие эффекты частиц
Серия JavaScript для продвинутых пользователей
- JavaScript углубил понимание прототипа и цепочки прототипов.
- Расширенное изучение JavaScript перед компиляцией
- JavaScript Advanced Полностью проясните этот наводящий вопрос в JS | «2w Word Big Chapter 38 Вопросы для интервью»
- Глубокое понимание EventLoop для продвинутого JavaScript
- Расширенная область действия JavaScript и цепочка областей видимости
часть интервью
- Резюме последних предварительных вопросов интервью Ниуке (включая анализ)
- Последний письменный тест Nioke по интерфейсу JS, 100 вопросов
- В 2021 году крупные компании, работающие с клиентами, ответили на эти вопросы, написанные от руки.
- Душевная пытка родного JavaScript (1), сколько вы можете ответить?
счастливое программирование
💥 Послесловие
Друзья, если вы чувствуете, что эта статья вам полезна, ставьте лайк Абао👍 или подписывайтесь➕ — это самая большая поддержка для меня.
Кроме того, если есть какие-либо проблемы с этой статьей или если вы не понимаете часть статьи, вы можете ответить мне в области комментариев, давайте обсудим, изучим и добьемся успеха вместе!
Если вы чувствуете, что область комментариев неясна, вы также можете добавить мой WeChat (li444186976) или qq (3315161861) для подробного общения, имя - пакет поля битвы.