JS第一周学习笔记整理

JS正式课第一周笔记整理

  • webstorm : 代码编辑器
  • 浏览器: 代码解析器;
  • Git : 是一个工具;用于团队协作开发项目管理代码的工具;在工作中用git、svn
  • svn : 集中式;
  • 集中式:需要一个中央服务器;每次开发前需要从中央服务器把最新的代码拉取下来,然后进行开发;并且需要网络;
  • git : 分布式;
  • 分布式:每个人的电脑都是一个中央服务器;不需要网络也可以进行提交代码;

DOS命令

  • 1.按着shift右键,在此处打开powerShell
  • 2.找到相应的文件夹,直接输入cmd,按回车;
  • window+r : 输入cmd

切换磁盘路径

  • cd + 文件夹名字: 打开相应的路径
  • cd ../ : 回到上一级
  • cd / : 回到跟路径;
  • mkdir + 文件名字 : 创建文件夹
  • cls : 清除所有的命令

Git

配置Git用户名和密码
  • git config --global user.email "邮箱"
  • git config --global user.name "用户名"
Git的简单命令
  • git init : 初始化一个git 仓库(生产.git文件)
  • git add . : 把所有文件提交到暂存区
  • git add [文件名]:推送指定的文件到暂存区
  • git commit -m"注释" : 把代码提交到历史区;(推送到历史区之后Git会自动分配一个版本号xxxxxx,根据版本号可以回到任何一次修改的位置)
  • git status:查看文件状态;如果文件只存在本地,不在暂存区,那么颜色是红色,如果文件在暂存区还没有提交到历史区,那么颜色是绿色
  • 本地代码- ->暂存区- ->历史区
本地仓库与远程仓库通信
  • git remote add origin(仓库名) url地址:将本地仓库与远程仓库建立连接
  • git remote -v:查看本地仓库和远程仓库关联状态
  • git push origin(与建立连接时的仓库名一样) master(主分支名):将历史区内容推送到远程仓库
  • git config --list:查看本地配置环境
  • 输入用户名和密码
推送过程(详细步骤)
本地操作
  • ① git init(初始化git仓库)
  • ② git add .(将文件推送到暂存区)
  • ③ git status(查看文件状态)
  • ④ git commit -m "注释"(将代码提交到历史区,并添加注释)
远程仓库操作
  • git remote add origin(仓库名) url地址
  • git remote -v
  • git push origin(仓库名) master
    Alt text
推送复原过程

Alt text

拉取克隆过程(详细步骤)
  • git clone 仓库地址url:这里已经是一个git仓库,并且已经和远程建立和连接
  • git pull origin master:拉取远程仓库代码
    Alt text

GitHub

  • GitHib:是一个开源网站,可以供大家分享代码 插件和框架
  • 把本地的代码提交到远程仓库(GitHub)
  • 让本地仓库和远程仓库进行绑定

作用域

作用域:代码执行的空间环境 ===栈内存

浏览器的渲染机制:先形成一个DOM树和CSS树,最后两个树合成render树

全局作用域 : 当打开浏览器时,浏览器会形成一个全局的作用域,给当前代码提供运行环境的;并且存储基本数据类型值;

  • 存储基本数据类型值
  • 基本数据类型存储到栈内存中,全局作用域是最大的一个栈内存
  • window是全局中最大的一个对象,存储了大量的内置属性

私有作用域:一个函数执行的时候就会形成一个私有作用域,函数执行完成(或关闭浏览器)就销毁
块级作用域:ES6新增的,除了私有作用域和对象以外所有被一个{}包裹起来的,(三个判断,三个循环)eval("({a:1})")、for 循环中用let ,会形成父作用域;每循环一次,都会形成一个子作用域;在子作用域中会把当前的i进行保存

全局变量:在全局作用域下定义的变量
  • 会给window新增一个键值对
  • 全局下定义的函数相当于给window新增键值对,属性名是函数,属性值是整个函数
私有变量:
  • 在函数体中被var 、function、const、let声明
  • 函数的形参也是私有变量
性能优化
//1-
function fn(){
//传入任意参数求和
	var total = 0;
	var len = arguments.length;
//(var i = 0;i<arguments.lenth;i++)
	for(var i = 0;i<len;i++){
		var cur = arguments[i];
		if(!isNaN(cur)){
		//total += arguments[i];
		total += cur;
		}
	}
}
//2- in 方法用于检测对象中是否有该属性
function fn(){}
console.log('fn' in window);//true
//'fn'必须加引号,不叫引号就是这个变量对应的值

//3-
var a,b,fn = function(){
	var a = b = 10;
	//(var a = b = 10;-->var a = 10;b = 10;)
	//a变成了局部变量无法修改外部,b是全局变量,修改了外部
}
fn();
console.log(a,b);//undefined 10

变量提升

https://blog.csdn.net/zjy_android_blog/article/details/80863425

https://blog.csdn.net/longwenjunjie/article/details/48163293

变量提升的定义

变量提升:在作用域形成之后代码执行之前,将所有带var和function关键字的变量提前声明或定义

JS可以在浏览器中农运行 是因为浏览器给JS提供了赖以生存的环境,这个环境就是作用域

  • var:只是提前声明;默认值是undefined
  • function:既声明又定义
  • debugger:打断点

形成作用域-变量提升-代码从上到下执行

变量提升的特殊情况
  • 不管条件是否成立,都要进行变量提升
  • 条件判断中的函数,在条件判断之前,带var和带function都只是声明不定义
  • 当条件成立之后,第一件事就是给函数复制;
  • 条件不成立,判断体就不执行,结合ES6的块级作用域
  • 如果不带var直接赋值就相当于给window直接添加了一个属性 并且给它赋值
  • var就相当于子安全局作用域下增加了一个属性,还给全局对象window增加了一个属性
  • 注意: 带var 的可以不赋值,但是也相当于给全局对象window增加了一个属性. 不带var的必须要赋值,否则就会去找全局对象window上是否有这个属性,没有这个属性就会报错.
  • 用逗号分隔的表示连var;如:var a = 1,b = 2,c = 3;
  • 用分号(分号代表代码段)来分隔的,前面带var的属于变量,不带var的相当于给全局对象window添加了属性;如:var a = 1;b = 2;c = 3;
  • 连等 带var的是一个变量,其他的不带var的就相当于给全局对象window添加了属性
  • 变量提升只发生在=左边
  • return后面的内容不进行变量提升 但是下面的 代码要进行变量提升
function f3(){
	console.log(f4());//打印f3函数体中的内容
	console.log(f2());//报错f2没有进行变量提升
	return function f2(){
	//return 后面的内容不进行变量提升 但是下面的代码要进行变量提升
	}
	function f4(){
		console.log(f3);
	}
}
f3();

var a = [1];
b = a;
b[1] = 'a';//b = [1,'a']
console.log(a);//[1,'a'] b指向了a的地址,修改的时候修改了地址中的属性以及属性值
  • 变量名重复:不再进行声明,但是要重新定义 (函数名跟变量名重复取函数名、函数名和函数名重复会覆盖)
  • 函数当做参数的时候,不进行变量提升
  • 匿名函数不进行变量提升
  • 自执行函数不会进行变量提升
  • let const声明的变量不进行变量提升

函数的定义与执行

  • 定义的三步曲:
  • 开辟新的堆内存
  • 存储代码字符串
  • 返回内存空间地址
  • 函数执行过程:
  • 首先会形成一个私有作用域,开辟一个栈内存
  • 形参赋值
  • 变量提升
  • 代码从上到下执行
  • 作用域是否销毁
  • 对象的定义:
  • 开辟一个空间地址,堆内存
  • 把键值对存储到堆内存下
  • 把空间地址赋值给对象名
console.log(fn);//fn()
console.log(f);//undefined
function fn(){}//函数声明和定义
var f = function(){}//函数表达式-->箭头函数
console.log(fn);//fn函数体
console.log(f);//undefined
console.log(fn());//打印两次undefined 函数执行的时候打印f为undefined,并且函数没有返回值 打印一次undefined
function fn(){
console.log(f);
}
var f = function(){}//函数声明
console.log(f2);//f2未定义 报错
var 
var f = function f2(){}
console.log(f2)//f2未定义 报错
setTimeout(funtion f2(){},1000)//函数作为参数的时候不进行变量提升

函数声明提升

1、函数的两种创建方式
  • 函数声明
  • 函数表达式

函数声明的语法

f('nihao');
function f(name){
	console.log(name);
}
//能打印出nihao

函数表达式语法

f('nihao');
var f = function (name){
console.log(name);
}
//控制台报错 Uncaught ReferenceError:f id not function错误显示f不是一个函数

两种方式的区别:

  • 函数声明又一个非常重要的特征:函数声明提升,函数声明语句将会被外部脚本或者外部函数作用域的顶部(跟变量提升非长相似)。正是这个特征,所以可以把函数声明放在调用它的语句后面。
var getName = function (){
console.log(2);
}
function getName(){
console.log(1);
}
getName();
//打印出2

这个例子涉及到了变量声明提升函数声明提升.正如前面提及到的函数声明提升,函数声明function getName(){}的声明会被提前到顶部.而函数表达式var getName = function(){}则表现出变量声明提升.因此,在这种情况下,getName也是一个变量,这个变量的声明将被提升到底部,而变量的赋值依然保留在原来的位置.需要注意的是:函数优先,虽然函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量。因此上面的函数可以换成这个样子

function getName(){
//函数声明提升到顶部
console.log(1);
}
var getName;//变量声明提升
getName = function(){
//变量赋值依然保留在原来的位置
console.log(2);
}
getName();//最终输出2

在原来的例子中,函数声明虽然是在函数表达式之后,但是由于函数声明提升到顶部,因此后面getName又被函数表达式的赋值操作给覆盖了,所以控制台输出了2
Alt text
Alt text

console.log(f);//打印函数体
function f(){
	console.log(1);
}
f();//88
function f(){
	console.log(2);
}
function f(){
	console.log(3);
}
f();//88
f = 99;
function f(){
	console.log(88);
}
f();//f is not a function 
console.log(f);

ES6的let和const

var 与ES6中const 、let声明的变量的区别
https://blog.csdn.net/qq_22855325/article/details/72843456

let不能和函数重名

  • const声明的变量,
  • 如果是基本数据类型,那么不可以进行数据修改.
  • 如果 是引用数据类型值,就可以操作引用地址,不可以替换引用地址
  • 没有变量提升
  • 不可以重复声明
  • 定义的变量不会给window增加属性
  • 定义的是个常量,定义之后不可以修改
  • 一旦声明必须赋值
  • let虽然不会进行变量提升,但是会先检查当前作用域下是否有重复命名
  • 没有变量提升
  • 不可以重复声明
  • 定义的变量不会给window增加属性
var a = 2;
if('a' in window){
	console.log(a);//形成暂时性死区,即在块级作用域下,不能提前拿到let声明的变量的值,打印2
	let a = 1;
}
let

ES6中提出了一个新的变量,不在于取代var,而在于解决ES5中var声明中的一些痛点;这就是let
let的特点

  • 1、let是块级变量,不存在于window下[非全局属性],window,变量名是找不到的,它的作用范围就那么一小块
  • 2、let不允许重新声明同名变量,会抛出异常,具有唯一性
  • 3、let不允许没声明就使用,会抛出异常,只有执行该声明的时候才能使用
  • 4、let有自己特色的闭包特性,比如在for循环的应用中
//1-
let tt = 'nihao';
console.log(tt);//打印出'nihao'
console.log(window.tt);//打印出undefined

//2-
function test2(){
	var abc = 99;
	let abc = 88;
	console.log(abc);
}
test2();//打印值:Uncaught SyntaxError:Indentifier 'abc' has already been declared

//3-
function test3(){
	console.log(test3);
	let test3 = '哈哈哈哈';
}
test3();//打印值:Uncaught ReferenceError:test3 is not defined

//4-每一次for村换都重新绑定一次作用域且脱离失效,就是let自身的特色
for(let i = 0;i<9;i++){
	//for循环形成保护机制
	console.log('循环内的值是:'+i);
}
console.log(i);//打印值
//循环内的值是:0
//循环内的值是:1
//循环内的值是:2
//循环内的值是:3
//循环内的值是:4
//循环内的值是:5
//循环内的值是:6
//循环内的值是:7
//循环内的值是:8
//Uncaught ReferenceError:i is not defined  块级作用域 外界无法访问

查找上一级的作用域

1、在当前作用域下输出变量值时,首先观察是否是私有变量

  • 如何判断一个变量是私有的
  • 当前变量有没有被var过和function
  • 形参也是私有变量

2、如果变量不是私有的,就向上一级作用域查找

  • 上一级作用域判断函数在哪定义的,函数上一级的作用域就是谁,跟函数执行没有任何关系
  • 但获取变量值时,首先是否是私有变量,如果不是就向上一级作用域查找,一级一级向上,直到window为止,如果window也没有,那么就会报错,这样一级一级向上查找作用域就是作用域链

堆内存的销毁

形成两个虚拟内存:栈内存、堆内存
栈内存:作用域
引用数据类型
在JS中的{}[]都是开辟一个新的空间地址
谷歌浏览器:每隔一段时间,自动检查占用并是否销毁
火狐和IE:采用的计数的规则,当堆内存地址被占用一次时,计数+1,否则-1,如果是被占用0次,就回收

私有作用域的销毁

  • 函数的执行:形成一个私有的作用域,提供代码运行环境,存储基本数据类型
闭包
  • 保护里面的私有变量不受外界干扰
  • 存储值
  • 保护机制:当前私有作用域中,有引用数据类型被外界所占有,导致当前作用域变成一个不销毁的作用域,里面的变量就成了不销毁的变量

一般情况下,函数执行完成之后,当前作用域就会销毁
函数没执行一次就会开辟一个新的私有作用域,并且新的私有作用域和之前的作用域没有任何关系,是两个不同的栈内存

  • 不销毁的作用域:
  • 函数执行return出一个引用的数据类型值
  • 函数执行return出的引用数据类型值并且被外界接收(被占用)

栈内存

浏览器加载的时候,碰到引用数据类型,都会开辟一个新的内存空间,[对象:键值对;函数:代码字符串],给这个内存空间赋一个16进制内存地址,将这个内存地址指向声明的变量,这个内存空间就是堆内存
堆内存的释放,手动赋值null,[指向空指针];浏览器判断该内存没有变量就去收回它,就会释放

function fn(){
	var t = 10;
	return function (){
		console.log(t++)
	}
}
var f = fn();
f();
f();
  • 不立即销毁:
  • 需要等到里面的小函数执行完成之后,那么外层作用域就会销毁
function fn(){
	var t = 10;
	return function (){
		console.log(t++);
	}
}
fn()();
var i = 0;
function fn(){
	//i = 5 6;
	return function(n){
		console.log(n + i++);
	}
}
var f = fn(5);
f(10);//15
f(20);//26
fn(8)(12);//20
fn()(18);//NaN
function fn (){
var a = 1 ;
return function (){
a++;
console.log(a);
}
}
var f = fn();
f(); //2
fn()(); //2
f(); //3
var obj= {
 i : 10,
fn:(function () {
var i = 0;
return function (n) {
console.log(n + i++)
}
})()
};
var f = obj.fn;
f(10);// 10
f(20);// 21
obj.fn(30);// 32 块级作用域
obj.fn(40);// 43

this关键字

  • 函数中的this,指的就是函数的当前执行主体
  • 1、在全局作用域下,this 指向window
  • 2、函数体中的this,看函数执行前有没有'.';如果有,那么'.'前面是谁,this就指向谁;如果没有'.',那么会指向window
  • 3、如果给元素的事件行为绑定方法,,那么方法中的this,就会指向当前被绑定的那个元素
  • 4、回调函数中的this指向window
  • 5、自执行函数中的this一般都指向window
  • 6、forEachmap第二个参数可以修改回调函数中的this
  • 7、构造函数中的this指向当前类的实例
  • 8、call、apply、bind可以改变this关键字的指向
  • this是谁,和它在哪定义的以及在哪执行的没有任何关系
    Alt text
    Alt text
    Alt text
function b() {
	console.log(this); //window
}
window.b();
var obj = {
	num:1,
	fn : function () {
		console.log(this);
		function m() {
			console.log(this);// window;
		}
		m()
	}
}
var f = obj.fn;
f(); // window
obj.fn();// obj
var obj = {
	num:1,
	f:{
		num:2,
		fn:function () {
		console.log(this);// obj.f
		}
	}
}
obj.f.fn()
//1.
setInterval(function () {
	console.log(this); // window
},1000)
//2.
(function () {
	console.log(this); //window
})()
//3.
var obj = {
	fn: (function () {
		console.log(this); //window
})()
}
var num =2;// 1 2
var obj = {
	num : 0,
	fn : function () {
		num = 1;
		// this-->obj
		(function (num) {
		// this --> window
			++this.num;
			num++;
			console.log(num)// 1
		})(this.num)// 0
	}
}
obj.fn();
console.log(window.num,obj.num) //2 0

this的重点练习题

var num =2;// 1 2
var obj = {
	 num : 0,
	 fn : function () {
	 num = 1;
     // this-->obj
	(function (num) {
	// this --> window
		++this.num;
		num++;
		console.log(num)// 1
	})(this.num)// 0
  }
}
obj.fn();
console.log(window.num,obj.num) //2 0

Alt text
Alt text

设计模式-单例模式

模块化开发

把一个项目按照页面或者功能分割成不同的任务,交给项目中不同的成员开发.开发结束之后
将代码合并到一起.

  • 多人协作开发的问题: 变量名 函数名 冲突
  • 为了防止全局变量污染: 闭包但是闭包并不完美,所以选择搭配设计模式来进一步进行
    项目开发
单例模式
  • 表现形式:就是一个对象:{name:xxx,age:function(){}};
  • 定义:把描述同一件事物的不同属性放在同一个对象[空间]下,避免了全局变量的干扰;这种模式就是单例模式
  • 在设计模式当中,这个person不仅仅是一个对象名,还是一个命名空间,多个命名空间是相互独立的,互不干扰
  • 好处:不受全局变量的污染和干干扰,并且可以相互调用方法
  • 由来:单例模式的本质就是一个对象,它是object类上的一个实例,实例与实例之间互不干扰,叫做单独的实例,简称"单例"
高级单例模式
  • 定义:在高级单例模式当中,我们不只会把一个对象赋值给一个命名空间,我们会先让它执行一个自执行函数,[就形成了一个闭包,一个不销毁的作用域],在自执行函数中,返回一个对象给这个命名空间
  • 好处:可以在私有的作用域当中,创建很多的属性和方法,仅需要把我们使用的属性和方法暴露在这个对象当中即可。相对于单例模式,可以存储不同方法中的公有变量
工厂模式
  • 定义:把实现同一种功能的代码放进一个函数中,当想实现类似功能时,只需要执行这个函数即可,传参数不同就可以.
  • 好处:减少了代码的冗余:"高内聚、低耦合"-->函数的封装

面向对象

  • OOP/OP面向对象的缩写,面向对象思想
  • 定义:面向对象的封装、继承和多态,通过简单的实例化,调用其他的方法和属性
  • JS就是通过面向对象的思想设计开发出来的
  • 需要了解 对象、类、实例
  • 对象:万物皆对象,多为一种泛指,可以是任何物体
  • 类:对象中具有同一属性和特征的集合,类又分为大类和小类
  • 实例:就是类中的一个具体的细分,我们研究当前实例具有哪些属性和方法,那么证明当前类中的其他实例也同样具有
JS中的内置类
  • JS中的任何数据类型都是它所属的类,除了null、undefined
  • Number、String、Boolean、Object、Array RegExp、Function、Date...
  • 类的首字母都是大写
  • 类的执行通过new来执行
  • Object对象类,被称为基类,在任何数据类型都可以通过__proto__[原型链]找到基类Object
创建类
  • 字面量方式
  • 实例化方式
  • 引用数据类型方式创建 var ary = new Array('1')
    注意:
  • new Array(10):创建一个长度为10的数组,数组中的每一项都是空
  • new Array('10'):如果只传递一个实参,并且实参不是数字,相当于把当前值作为数组的第一项存储进来
  • new Array(10,20,30):如果传递多个实参,不是设置长度,而是把传递的内容当做数组中的每一项存储起来
构造函数
  • 定义:当一个函数通过new关键字来执行的时候,这个函数就不是普通函数了,它是一个构造函数,也是一个自定义类,当前的函数名就是类名,这个函数的返回值就是这个类的实例
  • 为了让构造函数和普通函数有一个区别,我们建议写构造函数的时候首字母大写[类名]
  • 注意:一般情况下,我们写业务逻辑的时候不会用到构造函数,但是在封装库、插件以及组件的时候就会用到构造函数模式
  • 构造函数执行的时候,如果没有参数的时候,小括号可以不写
  • let person = new Person
普通函数和构造函数的区别
  • 相同点:形成私有的作用域-->形参赋值-->变量提升-->从上到下执行-->作用域销毁
  • 不同点:
    • 构造函数运行时,形成作用域后,在代码运行之前,首先会给当前的作用域初始化一个空对象;并且让当前作用域下的this指向这个空对象 当代码运行结束,构造函数把函数体中的this作为返回值返回
    • 构造函数如果并不需要传参,可以省略执行的小括号
    • 构造函数中的this,指向当前的实例
    • 在构造函数中,return 一个基本数据类型值,那么对实例没有任何影响;如果return出一个引用数据类型值,那么会把默认returnthis替换掉.
构造函数的执行过程
  • 形成一个私有的作用域
  • 形参赋值
  • 变量提升
  • 浏览器会创建一个对象,[开辟一个新的堆内存],将这个对象指向了this[堆内存指针指向this实例]
  • 代码从上到下执行
  • 判断当前构造函数是否有return,
  • 如果没有return默认将实例返回;
  • 如果有return,
    • 如果return的是基本数据类型,对实例没有影响
    • 如果是引用数据类型,那么实例就是该引用数据类型
  • 构造函数中:建议不要轻易return引用数据类型
    Alt text
私有属性
  • 在构造函数中,给this添加属性值和方法,都属于当前实例的私有属性
公有属性
  • 当前实例通过__proto__找到所有的属性和方法都输属于当前实例的公有属性
  • 实例想要调取公有属性,直接可以调取,底层及时通过__proto__去找这个属性
  • 用in这种方式来判断,当前属性名[公有属性+私有属性]是都属于这个对象
    console.log('hasOwnProperty' in person1);
  • Object类提供一个hasOwnProperty,这个方法判断当前属性是否是该实例的私有属性:返回布尔值console.log(person1.hasOwnProperty("age")) //true console.log(person1.hasOwnProperty("valueof")) //false
    例题:自己封装一个方法,判断当前属性是否是当前实例的一个公有属性hasPubProperty
function hasPubProperty(obj,item){
        //先检测是否是属性
        return item in obj &&!obj.hasOwnProperty(item);
    }
    console.log(hasPubProperty([], 'toString'));
JS中的函数
  • 普通函数、类(自定义类和内置类)、函数类的实例
对象
  • 普通对象(对象数据类型)
  • 构造函数new出来的一个实例,也是一个对象
  • 类上面的原型也是一个对象
  • 函数也是一个对象
学习原型模式需要记住三句话
  • 所有的函数都天生自带一个属性,叫做prototype(原型),它是一个对象,既然是对象,那就是一个堆内存

  • 所有函数的原型上面(都是开辟的这个堆内存),都天生自带一个属性,叫做constructor(构造函数),它指向当前类本身

  • 所有的对象都天生自带一个属性__proto__,它指向当前类的原型

  • 所有的函数数据类型(普通函数、类(内置的、自定义))都是Function的一个实例;Function是所有函数的基类;

  • 5.所有的对象数据类型(实例、prototype、对象)都是Object的一个实例;Object是所有对象数据类型的基类;

  • Function 首先是自己的一个实例;
    Alt text

function People(name){
	let age = 9;
	this.name = name;
	this.age = age;
}
People.prototype.say = function () {
	console.log(this);
};
let person1 = new People('zf');
let person2 = new People('zhufeng');
//person1属于People类
//__proto__指向类People的原型prototype
console.log(person1.__proto__ == People.prototype);//true

Alt text

原型链
  • 定义:一个实例要找属性,优先会去找自己的私有属性,如果自己的私有属性没有,那就通过__proto__找到自己所属类的原型上面的公有属性,如果公有属性还没有,继续通过__proto__找到自己的所属类的原型直到Object[基类]的原型上,一直找到基类还没有的话,直接返回undefined
Object类和Function类

Function类:

  • 所有的类都是通过函数的方式来创建,由此可以得知,所有的类(包括基类Object)都是函数类的一个实例
  • 判断一个实例是否属于一个类:intanceof
    console.log(Number instanceof Function); //true
    console.log(String instanceof Function); //true
    console.log(Object instanceof Function); //true
  • constructor:通过实例来调用这个方法,指向当前类本身
  • 万物皆对象,JS中的任何数据类型都可以通过proto
    Alt text
posted @ 2018-07-15 22:17  kjz  阅读(608)  评论(2编辑  收藏  举报