先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
接着上一个题目的风格,稍微变点花样:
var obj1 = {
a: 1
}
var obj2 = {
a: 2,
bar: function () {
console.log(this.a)
},
foo: function () {
setTimeout(function () {
console.log(this)
console.log(this.a)
}.call(obj1), 0)
}
}
var a = 3
obj2.bar()
obj2.foo()
乍一看上去,这个题看起来有些莫名其妙, setTimeout
那是传了个什么东西?
做题之前,先了解一下 setTimeout
的内部机制:(关于异步的执行顺序,可以参考 JavaScript之EventLoop [6] )
setTimeout(fn) {
if (回调条件满足) (
fn
)
}
这样一看,本题就清楚多了,类似 题目4.2
,修正了回调函数内 fn
的 this
指向。
答案
2
{a: 1}
1
题目4.4:注意call位置
function foo () {
console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
-
foo()
: 默认绑定 -
foo.call(obj)
: 显式绑定
foo().call(obj)
foo()
call
foo
undefined
call()
答案
2
1
2
Uncaught TypeError: Cannot read property ‘call’ of undefined
题目4.5:注意call位置(2)
上面由于 foo
没有返回函数,无法执行 call
函数报错,因此修改一下 foo
函数,让它返回一个函数。
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
-
foo()
: 默认绑定 -
foo.call(obj)
: 显式绑定
foo().call(obj)
foo()
2
call
this
obj
1
这里千万注意:最后一个 foo().call(obj)
有两个函数执行,会打印 2个值 。
答案
题目4.6:bind
将上面的 call
全部换做 bind
函数,又会怎样那?
call是会立即执行函数,bind会返回一个新函数,但不会执行函数
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo()
foo.bind(obj)
foo().bind(obj)
首先我们要先确定,最后会输出几个值? bind
不会执行函数,因此只有两个 foo()
会打印 a
。
-
foo()
: 默认绑定,打印2
-
foo.bind(obj)
: 返回新函数,不会执行函数,无输出
foo().bind(obj)
foo()
2
bind
foo()
this
obj
答案
题目4.7:外层this与内层this
做到这里,不由产生了一些疑问:如果使用 call、bind
等修改了外层函数的 this
,那内层函数的 this
会受影响吗?(注意区别箭头函数)
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo.call(obj)()
foo.call(obj)
: 第一层函数 foo
通过 call
将 this
指向 obj
,打印 1
;第二层函数为匿名函数,默认绑定,打印 2
。
答案
题目4.8:对象中的call
把上面的代码移植到对象中,看看会发生怎样的变化?
var obj = {
a: ‘obj’,
foo: function () {
console.log(‘foo:’, this.a)
return function () {
console.log(‘inner:’, this.a)
}
}
}
var a = ‘window’
var obj2 = { a: ‘obj2’ }
obj.foo()()
obj.foo.call(obj2)()
obj.foo().call(obj2)
看着这么多括号,是不是感觉有几分头大。没事,咱们来一层一层分析:
obj.foo()()
obj.foo()
foo:obj
inner:window
obj.foo.call(obj2)()
题目4.7
obj.foo.call(obj2)
call
obj.foo
this
obj2
foo: obj2
inner:window
obj.foo().call(obj2)
题目4.5
foo: obj
call
this
obj2
inner: obj2
题目4.9:带参数的call
显式绑定一开始讲的时候,就谈过 call/apply
存在传参差异,那咱们就来传一下参数,看看传完参数的this会是怎样的美妙。
var obj = {
a: 1,
foo: function (b) {
b = b || this.a
return function © {
console.log(this.a + b + c)
}
}
}
var a = 2
var obj2 = { a: 3 }
obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
要注意 call
执行的位置:
-
obj.foo(a).call(obj2, 1)
: -
obj.foo(a)
: foo的AO中b值为传入的a(形参与实参相统一),值为2,返回匿名函数fn -
匿名函数
fn.call(obj2, 1)
: fn的this指向为obj2,c值为1 -
this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6
-
obj.foo.call(obj2)(1)
: -
obj.foo.call(obj2)
: obj.foo的this指向obj2,未传入参数,b = this.a = obj2.a = 3;返回匿名函数fn -
匿名函数
fn(1)
: c = 1,默认绑定,this指向window -
this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6
答案
麻了吗,兄弟们。进度已经快过半了,休息一会,争取把 this
一次性吃透。
5.显式绑定扩展
上面提了很多 call/apply
可以改变 this
指向,但都没有太多实用性。下面来一起学几个常用的 call与apply
使用。
题目5.1:apply求数组最值
JavaScript中没有给数组提供类似max和min函数,只提供了 Math.max/min
,用于求多个数的最值,所以可以借助apply方法,直接传递数组给 Math.max/min
const arr = [1,10,11,33,4,52,17]
Math.max.apply(Math, arr)
Math.min.apply(Math, arr)
题目5.2:类数组转为数组
ES6
未发布之前,没有 Array.from
方法可以将类数组转为数组,采用 Array.prototype.slice.call(arguments)
或 [].slice.call(arguments)
将类数组转化为数组。
题目5.3:数组高阶函数
日常编码中,我们会经常用到 forEach、map
等,但这些数组高阶方法,它们还有第二个参数 thisArg
,每一个回调函数都是显式绑定在 thisArg
上的。
例如下面这个例子
const obj = {a: 10}
const arr = [1, 2, 3, 4]
arr.forEach(function (val, key){
console.log(${key}: ${val} --- ${this.a}
)
}, obj)
答案
0: 1 — 10
1: 2 — 10
2: 3 — 10
3: 4 — 10
关于数组高阶函数的知识可以参考: JavaScript之手撕高阶数组函数
6.new绑定
使用 new
来构建函数,会执行如下四部操作:
-
创建一个空的简单
JavaScript
对象(即{}
); -
为步骤1新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; -
将步骤1新创建的对象作为
this
的上下文 ; -
如果该函数没有返回对象,则返回
this
。
关于new更详细的知识,可以参考: JavaScript之手撕new [7]
通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this。
题目6.1:new绑定
function User(name, age) {
this.name = name;
this.age = age;
}
var name = ‘Tom’;
var age = 18;
var zc = new User(‘zc’, 24);
console.log(zc.name)
答案
zc
题目6.2:属性加方法
function User (name, age) {
this.name = name;
this.age = age;
this.introduce = function () {
console.log(this.name)
}
this.howOld = function () {
return function () {
console.log(this.age)
}
}
}
var name = ‘Tom’;
var age = 18;
var zc = new User(‘zc’, 24)
zc.introduce()
zc.howOld()()
这个题很难不让人想到如下代码,都是函数嵌套,具体解法是类似的,可以对比来看一下啊。
const User = {
name: ‘zc’;
age: 18;
introduce = function () {
console.log(this.name)
}
howOld = function () {
return function () {
console.log(this.age)
}
}
}
var name = ‘Tom’;
var age = 18;
User.introduce()
User.howOld()()
-
zc.introduce()
: zc是new创建的实例,this指向zc,打印zc
-
zc.howOld()()
: zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18(阿包永远 18 )
答案
zc
18
题目6.3:new界的天王山
new
界的天王山,每次看懂后,没过多久就会忘掉,但这次要从根本上弄清楚该题。
接下来一起来品味品味:
function Foo(){
getName = function(){ console.log(1); };
return this;
}
Foo.getName = function(){ console.log(2); };
Foo.prototype.getName = function(){ console.log(3); };
var getName = function(){ console.log(4); };
function getName(){ console.log(5) };
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
- 预编译
GO = {
Foo: fn(Foo),
getName: function getName(){ console.log(5) };
}
- 分析后续执行
-
Foo.getName()
: 执行Foo上的getName方法,打印2
-
getName()
: 执行GO中的getName方法,打印4
-
Foo().getName()
// 修改全局GO的getName为function(){ console.log(1); }
getName = function(){ console.log(1) }
// Foo为默认绑定,this -> window
// return window
return this
复制代码
-
Foo().getName()
: 执行window.getName(),打印1
-
Foo()
执行 -
getName()
: 执行GO中的getName,打印1
- 分析后面三个打印结果之前,先补充一些运算符优先级方面的知识(图源: MDN [8] )
从上图可以看到,部分优先级如下: new(带参数列表) = 成员访问 = 函数调用 > new(不带参数列表)
new Foo.getName()
首先从左往右看: new Foo
属于不带参数列表的new(优先级 19 ), Foo.getName
属于成员访问(优先级 20 ), getName()
属于函数调用(优先级 20 ),同样优先级遵循从左往右执行。
-
Foo.getName
执行,获取到Foo上的getName
属性 -
此时原表达式变为
new (Foo.getName)()
,new (Foo.getName)()
为带参数列表(优先级 20 ),(Foo.getName)()
属于函数调用(优先级 20 ),从左往右执行
new (Foo.getName)()
2
Foo.getName()
这里有一个误区:很多人认为这里的 new
是没做任何操作的的,执行的是函数调用。那么如果执行的是 Foo.getName()
,调用返回值为 undefined
, new undefined
会发生报错,并且我们可以验证一下该表达式的返回结果。
console.log(new Foo.getName())
// 2
// Foo.getName {}
可见在成员访问之后,执行的是 带参数列表格式的new 操作。
new Foo().getName()
步骤4
new Foo()
Foo
Foo
getName
Foo.prototype.getName
3
new new Foo().getName()
从左往右分析: 第一个new不带参数列表(优先级 19 ), new Foo()
带参数列表(优先级 20 ),剩下的成员访问和函数调用优先级都是 20
new Foo()
执行,返回一个以Foo
为构造函数的实例
Foo
Foo.prototype
getName
new (new Foo().getName)()
Foo.prototype.getName()
3
new Foo.getName()
与new new Foo().getName()
区别:
-
new Foo.getName()
的构造函数是Foo.getName
-
new new Foo().getName()
的构造函数为Foo.prototype.getName
测试结果如下:
foo1 = new Foo.getName()
foo2 = new new Foo().getName()
console.log(foo1.constructor)
console.log(foo2.constructor)
输出结果:
2
3
ƒ (){ console.log(2); }
ƒ (){ console.log(3); }
通过这一步比较应该能更好的理解上面的执行顺序。
答案
兄弟们,革命快要成功了,再努力一把,以后this都小问题啦。
7.箭头函数
箭头函数没有自己的 this
,它的 this
指向外层作用域的 this
,且指向函数定义时的 this
而非执行时。
this指向外层作用域的this
this
this
指向函数定义时的this而非执行时
:JavaScript
是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。更详细的介绍见 JavaScript之静态作用域与动态作用域 [9] 。
题目7.1:对象方法使用箭头函数
name = ‘tom’
const obj = {
name: ‘zc’,
intro: () => {
console.log('My name is ’ + this.name)
}
}
obj.intro()
上文说到,箭头函数的 this
通过作用域链查到, intro
函数的上层作用域为 window
。
答案
My name is tom
题目7.2:箭头函数与普通函数比较
name = ‘tom’
const obj = {
name: ‘zc’,
intro:function () {
return () => {
console.log('My name is ’ + this.name)
}
},
intro2:function () {
return function() {
console.log('My name is ’ + this.name)
}
}
}
obj.intro2()()
obj.intro()()
obj.intro2()()
: 不做赘述,打印My name is tom
obj.intro()()
obj.intro()
this
this
obj
My name is zc
题目7.3:箭头函数与普通函数的嵌套
name = ‘window’
const obj1 = {
name: ‘obj1’,
intro:function () {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
const obj2 = {
name: ‘obj2’,
intro: ()=> {
console.log(this.name)
return function() {
console.log(this.name)
}
}
}
const obj3 = {
name: ‘obj3’,
intro: ()=> {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
obj1.intro()()
obj2.intro()()
obj3.intro()()
obj1.intro()()
题目7.2
obj1,obj1
obj2.intro()()
obj2.intro()
this
this
window
window,window
obj3.intro()()
obj3.intro()
obj2.intro()
intro
this
window
window,window
答案
obj1
obj1
window
window
window
window
题目7.4:new碰上箭头函数
function User(name, age) {
this.name = name;
this.age = age;
this.intro = function(){
console.log('My name is ’ + this.name)
},
this.howOld = () => {
console.log('My age is ’ + this.age)
}
}
var name = ‘Tom’, age = 18;
var zc = new User(‘zc’, 24);
zc.intro();
zc.howOld();
zc
new User
User
this
zc
-
zc.intro()
: 打印My name is zc
-
zc.howOld()
:howOld
为箭头函数,箭头函数 this由外层作用域决定,且指向函数定义时的this ,外层作用域为User
,this
指向zc
,打印My age is 24
题目7.5:call碰上箭头函数
箭头函数由于没有 this
,不能通过 call\apply\bind
来修改 this
指向,但可以通过修改外层作用域的 this
来达成间接修改
var name = ‘window’
var obj1 = {
name: ‘obj1’,
intro: function () {
console.log(this.name)
return () => {
console.log(this.name)
}
},
intro2: () => {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var obj2 = {
name: ‘obj2’
}
obj1.intro.call(obj2)()
obj1.intro().call(obj2)
obj1.intro2.call(obj2)()
obj1.intro2().call(obj2)
obj1.intro.call(obj2)()
call
this
obj2
obj2
this
this
obj2
obj1.intro().call(obj2)
obj1
call
this
this
obj1
obj1.intro2.call(obj2)()
call
window
window
window
obj1.intro2().call(obj2)
window
call
this
obj2
obj2
答案
obj2
obj2
obj1
obj1
window
window
window
obj2
8.箭头函数扩展
总结
this
this
this
this
-
不可以用作构造函数,不能使用
new
命令,否则会报错 -
箭头函数没有
arguments
对象,如果要用,使用rest
参数代替 -
不可以使用
yield
命令,因此箭头函数不能用作Generator
函数。
call/apply/bind
this
this
- 箭头函数没有
prototype
属性。
避免使用场景
- 箭头函数定义对象方法
const zc = {
name: ‘zc’,
intro: () => {
// this -> window
console.log(this.name)
}
}
zc.intro() // undefined
- 箭头函数不能作为构造函数
const User = (name, age) => {
this.name = name;
this.age = age;
}
// Uncaught TypeError: User is not a constructor
zc = new User(‘zc’, 24);
- 事件的回调函数
DOM中事件的回调函数中this已经封装指向了调用元素,如果使用构造函数,其this会指向window对象
document.getElementById(‘btn’)
.addEventListener(‘click’, ()=> {
console.log(this === window); // true
})
9.综合题
学完上面的知识,是不是感觉自己已经趋于化境了,现在就一起来华山之巅一决高下吧。
题目9.1: 对象综合体
var name = ‘window’
var user1 = {
name: ‘user1’,
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var user2 = { name: ‘user2’ }
user1.foo1()
user1.foo1.call(user2)
user1.foo2()
user1.foo2.call(user2)
user1.foo3()()
user1.foo3.call(user2)()
user1.foo3().call(user2)
user1.foo4()()
user1.foo4.call(user2)()
user1.foo4().call(user2)
这个题目并不难,就是把上面很多题做了个整合,如果上面都学会了,此题问题不大。
-
user1.foo1()、user1.foo1.call(user2)
: 隐式绑定与显式绑定 -
user1.foo2()、user1.foo2.call(user2)
: 箭头函数与call -
user1.foo3()()、user1.foo3.call(user2)()、user1.foo3().call(user2)
: 见题目4.8 -
user1.foo4()()、user1.foo4.call(user2)()、user1.foo4().call(user2)
: 见题目7.5
答案:
var name = ‘window’
var user1 = {
name: ‘user1’,
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var user2 = { name: ‘user2’ }
user1.foo1() // user1
user1.foo1.call(user2) // user2
user1.foo2() // window
user1.foo2.call(user2) // window
user1.foo3()() // window
user1.foo3.call(user2)() // window
user1.foo3().call(user2) // user2
user1.foo4()() // user1
user1.foo4.call(user2)() // user2
user1.foo4().call(user2) // user1
题目9.2:隐式绑定丢失
var x = 10;
var foo = {
x : 20,
bar : function(){
var x = 30;
console.log(this.x)
}
};
foo.bar();
(foo.bar)();
(foo.bar = foo.bar)();
(foo.bar, foo.bar)();
突然出现了一个代码很少的题目,还乍有些不习惯。
-
foo.bar()
: 隐式绑定,打印20
-
(foo.bar)()
: 上面提到过运算符优先级的知识,成员访问与函数调用优先级相同,默认从左到右,因此括号可有可无,隐式绑定,打印20
(foo.bar = foo.bar)()
foo.bar
foo.bar
foo
10
(foo.bar, foo.bar)()
: 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10
。
上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足 XXX.fn()
格式,如果破坏了这种格式,一般隐式绑定都会丢失。
题目9.3:arguments(推荐看)
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments0;
}
};
obj.method(fn, 1);
这个题要注意一下,有坑。
-
fn()
: 默认绑定,打印10 -
arguments[0]()
: 这种执行方式看起来就怪怪的,咱们把它展开来看看:
arguments: {
0: fn,
1: 1,
length: 2
}
复制代码
arguments: {
fn: fn,
1: 1,
length: 2
}
复制代码
fn
this
arguments
-
arguments[0]
: 这是访问对象的属性0?0不好理解,咱们把它稍微一换,方便一下理解: -
arguments
是一个类数组,arguments
展开,应该是下面这样:
题目9.4:压轴题(推荐看)
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
《MySql面试专题》
《MySql性能优化的21个最佳实践》
《MySQL高级知识笔记》
文中展示的资料包括:**《MySql思维导图》《MySql核心笔记》《MySql调优笔记》《MySql面试专题》《MySql性能优化的21个最佳实践》《MySq高级知识笔记》**如下图
关注我,点赞本文给更多有需要的人
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
.bar
foo.bar
foo
10
(foo.bar, foo.bar)()
: 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10
。
上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足 XXX.fn()
格式,如果破坏了这种格式,一般隐式绑定都会丢失。
题目9.3:arguments(推荐看)
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments0;
}
};
obj.method(fn, 1);
这个题要注意一下,有坑。
-
fn()
: 默认绑定,打印10 -
arguments[0]()
: 这种执行方式看起来就怪怪的,咱们把它展开来看看:
arguments: {
0: fn,
1: 1,
length: 2
}
复制代码
arguments: {
fn: fn,
1: 1,
length: 2
}
复制代码
fn
this
arguments
-
arguments[0]
: 这是访问对象的属性0?0不好理解,咱们把它稍微一换,方便一下理解: -
arguments
是一个类数组,arguments
展开,应该是下面这样:
题目9.4:压轴题(推荐看)
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
《MySql面试专题》
[外链图片转存中…(img-3TwCzo99-1713380908114)]
[外链图片转存中…(img-x9m1T3GL-1713380908114)]
《MySql性能优化的21个最佳实践》
[外链图片转存中…(img-D3oCLIDh-1713380908115)]
[外链图片转存中…(img-dfQKvj3M-1713380908115)]
[外链图片转存中…(img-LYWa7M8V-1713380908115)]
[外链图片转存中…(img-OsEdNisg-1713380908115)]
《MySQL高级知识笔记》
[外链图片转存中…(img-EorhcDvG-1713380908116)]
[外链图片转存中…(img-LdiGcsjP-1713380908116)]
[外链图片转存中…(img-3ZDmxzjF-1713380908116)]
[外链图片转存中…(img-s8zpzxFi-1713380908116)]
[外链图片转存中…(img-KaCbN5vm-1713380908117)]
[外链图片转存中…(img-KtvVPzaL-1713380908117)]
[外链图片转存中…(img-MtTeyv9L-1713380908117)]
[外链图片转存中…(img-tgMAiNut-1713380908117)]
[外链图片转存中…(img-VV3q2rDM-1713380908117)]
[外链图片转存中…(img-fLFAk8Sv-1713380908118)]
文中展示的资料包括:**《MySql思维导图》《MySql核心笔记》《MySql调优笔记》《MySql面试专题》《MySql性能优化的21个最佳实践》《MySq高级知识笔记》**如下图
[外链图片转存中…(img-4hdmO6Lc-1713380908118)]
关注我,点赞本文给更多有需要的人
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-uxHCg95a-1713380908118)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!