【简明教程】JavaScript面向对象编程

随着HTML5标准的成熟和在移动开发领域的大规模使用,JavaScript正成为Web开发领域最热门的开发语言,而且随着NodeJS等技术的发展,JavaScript的应用也从传统前端开发领域延伸到了服务器端开发。但同时需要注意的是,我们项目中的JavaScript代码规模也变得越来越大和更加复杂。这就要求开发人员能够编写高效且可维护的JavaScript代码,虽然JavaScript不像Java那样对面向对象设计有那么好的支持,但我们可以通过在JavaScript中应用这些面向对象的设计模式,来使我们写出更优秀的JavaScript代码。

在这篇教程中,你将学习基于JavaScript的面向对象编程。其中的代码示例是基于EcmaScript 5(JavaScript的标准定义)来实现。

Java与JavaScript的比对

Java ** JavaScript**
静态类型 动态类型
使用类,接口和枚举来定义类型 使用函数和原型来定义类型
在运行时类型无法改变 类型可以在运行时变更
需要给所有变量声明类型(强类型校验) 声明变量时不需要指定类型 (弱类型校验)
构造器是特殊的方法 构造器也是一个函数,与其他函数没有区别
类和对象是不同的实体 包括构造器,函数原型在内的一切都是对象
支持静态方法和实例 不直接支持静态方法和实例
通过抽象类和接口支持抽象类型 不直接支持抽象类型
通过private,package,protected,public定义对象的作用域 只支持public成员
提供丰富的继承机制 通过原型实现继承
支持方法级的重写和重载机制 不直接支持重写和重载
提供丰富的反射特性 具有一些反射特性
通过包提供模块化支持 没有直接的模块化支持

对象类型定义- Object Type

function MyType(){
    if (!(this instanceof MyType))
        throw new Error("Constructor can’t be called as a function");
}

var myInstance = new MyType();
MyType(); // Error: Constructor can’t be called as a function

在Eclipse的JavaScript视图中,构造器,实例成员,静态成员和内部函数都能被识别,并在Outline视图中显示出来。

实例成员 - Instance Members

通过"new"关键字可以创建一个实例对象,而实例成员(变量或方法)能够通过这个实例对象来访问。实例成员可以通过"this"关键字,原型(prototype),构造器或Object.defineProperty来定义。

function Cat(name){
    var voice = "Meow";
    this.name = name;
    this.say = function(){
      return voice;
    }
}
Cat.prototype.eat = function(){
    return "Eating";
}
var cat = new Cat("Fluffy");
Object.defineProperty(cat, "numLegs",{value: 4,writable:true,enumerable:true,configurable:tr
ue});

console.log(cat.name); // Fluffy
console.log(cat.numLegs); // 4
console.log(cat.say()); // Meow
console.log(cat.eat()); // Eating

静态成员 - Static Members

JavaScript中并不直接支持静态成员。你可以通过构造器来创建静态成员。静态成员不允许通过"this"关键字直接访问。

公共静态成员

function Factory(){
}

// public static method
Factory.getType = function (){
    return "Object Factory";
};

// public static field
Factory.versionId = "F2.0";
Factory.prototype.test = function(){
    console.log(this.versionId); // undefined
    console.log(Factory.versionId); // F2.0
    console.log(Factory.getType()); // Object Factory
}

var factory = new Factory();
factory.test();

私有静态成员

var Book = (function () {
    // private static field
    var numOfBooks = 0;

    // private static method
    function checkIsbn(isbn) {
        if (isbn.length != 10 && isbn.length != 13)
            throw new Error("isbn is not valid!");
    }

    function Book(isbn, title) {
        checkIsbn(isbn);
        this.isbn = isbn;
        this.title = title;
        numOfBooks++;
        this.getNumOfBooks = function () {
            return numOfBooks;
        }
    }

    return Book;
})();

var firstBook = new Book("0-943396-04-2", "First Title");
console.log(firstBook.title); // First Title
console.log(firstBook.getNumOfBooks()); // 1

var secondBook = new Book("0-85131-041-9", "Second Title");
console.log(firstBook.title); // First Title
console.log(secondBook.title); // Second Title
console.log(firstBook.getNumOfBooks()); // 2
console.log(secondBook.getNumOfBooks()); // 2

抽象类型 - Abstract Types

JavaScript是一个弱类型语言,所以当你声明一个变量时,不需要指定它的类型。这就减弱了对于像接口这样的抽象类型的依赖。但有时候,你仍然希望使用抽象类型来将一些共有的功能放在一起,并采用继承的机制,让其他类型也具有相同的功能,你可以参考下面的示例:

(function(){
    var abstractCreateLock = false;

    // abstract type
    function BaseForm(){
        if(abstractCreateLock)
            throw new Error("Can’t instantiate BaseForm!");
    }
    
    BaseForm.prototype = {};
    BaseForm.prototype.post = function(){
        throw new Error("Not implemented!");
    }

    function GridForm(){
    }

    GridForm.prototype = new BaseForm();
    abstractCreateLock = true;
    GridForm.prototype.post = function(){
        // ...
        return "Grid is posted.";
    }
    
    window.BaseForm = BaseForm;
    window.GridForm = GridForm;
})();

var myGrid = new GridForm();
console.log(myGrid.post()); // Grid is posted.
var myForm = new BaseForm(); // Error: Can’t instantiate BaseForm!

接口 - Interfaces

JavaScript同样没有对接口的直接支持。你可以通过下面代码中实现的机制来定义接口。

var Interface = function (name, methods) {
    this.name = name;
    // copies array
    this.methods = methods.slice(0);
};

Interface.checkImplements = function (obj, interfaceObj) {
    for (var i = 0; i < interfaceObj.methods.length; i++) {
        var method = interfaceObj.methods[i];
        if (!obj[method] || typeof obj[method] !=="function")
            thrownewError("Interfacenotimplemented! Interface: " + interfaceObj.name + " Method: " + method);
    }
};

var iMaterial = new Interface("IMaterial", ["getName", "getPrice"]);

function Product(name,price,type){
    Interface.checkImplements(this, iMaterial);
    this.name = name;
    this.price = price;
    this.type = type;
}

Product.prototype.getName = function(){
    return this.name;
};
Product.prototype.getPrice = function(){
    return this.price;
};

var firstCar = new Product("Super Car X11",20000,"Car");
console.log(firstCar.getName()); // Super Car X11
delete Product.prototype.getPrice;
var secondCar = new Product("Super Car X12",30000,"Car"); // Error: Interface not implemented!

单例对象 - Singleton Object

如果你希望在全局范围内只创建一个某一类型的示例,那么你可以有下面两种方式来实现一个单例。

var Logger = {
    enabled:true,
    log: function(logText){
      if(!this.enabled)
        return;
      
      if(console && console.log)
        console.log(logText);
      else
        alert(logText);
    }
}

或者

function Logger(){
}
Logger.enabled = true;
Logger.log = function(logText){
    if(!Logger.enabled)
        return;

    if(console && console.log)
        console.log(logText);
    else
        alert(logText);
};
Logger.log("test"); // test
Logger.enabled = false;
Logger.log("test"); //

创建对象 - Object Creation

通过new关键字创建

可以使用"new"关键字来创建内置类型或用户自定义类型的实例对象,它会先创建一个空的实例对象,然后再调用构造函数来给这个对象的成员变量赋值,从而实现对象的初始化。

//or var dog = {};
//or var dog = new MyDogType();
var dog = new Object();
dog.name = "Scooby";
dog.owner = {};
dog.owner.name = "Mike";
dog.bark = function(){
   return "Woof";
};

console.log(dog.name); // Scooby
console.log(dog.owner.name); // Mike
console.log(dog.bark()); // Woof

通过字面量直接创建

通过字面量创建对象非常简单和直接,同时你还可以创建嵌套对象。

var dog = {
  name:"Scoobyî",
  owner:{
    name:"Mike"
  },
  bark:function(){
    return "Woof";
  }
};
console.log(dog.name); // Scooby
console.log(dog.owner.name); // Mike
console.log(dog.bark()); // Woof

成员作用域 - Scoping

私有字段 - Private Fields

在JavaScript中没有对私有字段的直接支持,但你可以通过构造器来实现它。首先将变量在构造函数中定义为私有的,任何需要使用到这个私有字段的方法都需要定义在构造函数中,这样你就可以通过这些共有方法来访问这个私有变量了。

function Customer(){
  // private field
  var risk = 0;
  this.getRisk = function(){
    return risk;
  };
  this.setRisk = function(newRisk){
    risk = newRisk;
  };
  this.checkRisk = function(){
    if(risk > 1000)
      return "Risk Warning";
    return "No Risk";
  };
}

Customer.prototype.addOrder = function(orderAmount){
  this.setRisk(orderAmount + this.getRisk());
  return this.getRisk();
};

var customer = new Customer();
console.log(customer.getRisk()); // 0
console.log(customer.addOrder(2000)); // 2000
console.log(customer.checkRisk()); // Risk Warning

私有方法 - Private Methods

私有方法也被称作内部函数,往往被定义在构造体中,从外部无法直接访问它们。

function Customer(name){
  var that = this;
  var risk = 0;
  this.name = name;
  this.type = findType();

  // private method
  function findType() {
     console.log(that.name);
     console.log(risk);
     return "GOLD";
   }
}

或者

function Customer(name){
  var that = this;
  var risk = 0;
  this.name = name;

  // private method
  var findType = function() {
     console.log(that.name);
     console.log(risk);
     return "GOLD";
  };
  this.type = findType();
}
var customer = new Customer("ABC Customer"); // ABC Customer
 // 0
console.log(customer.type); // GOLD
console.log(customer.risk); // undefined

如果私有内部函数被实例化并被构造函数返回,那么它将可以从外部被调用。

function Outer(){
  return new Inner();
  
  //private inner
  function Inner(){
     this.sayHello = function(){
        console.log("Hello");
     }
   }
}
(new Outer()).sayHello(); // Hello

特权方法 - Privileged Methods

原型方法中的一切都必须是公共的,因此它无法调用类型定义中的私有变量。通过在构造函数中使用"this."声明的函数称为特权方法,它们能够访问私有字段,并且可以从外部调用。

function Customer(orderAmount){
  // private field
  var cost = orderAmount / 2;
  this.orderAmount = orderAmount;
  var that = this;
  
  // privileged method
  this.calculateProfit = function(){
    return that.orderAmount - cost;
  };
}

Customer.prototype.report = function(){
  console.log(this.calculateProfit());
};

var customer = new Customer(3000);
customer.report(); // 1500

公共字段 - Public Fields

公共字段能够被原型或实例对象访问。原型字段和方法被所有实例对象共享(原型对象本身也是被共享的)。当实例对象改变它的某一个字段的值时,并不会改变其他对象中该字段的值,只有直接使用原型对象修改字段,才会影响到所有实例对象中该字段的值。

function Customer(name,orderAmount){
  // public fields
  this.name = name;
  this.orderAmount = orderAmount;
}

Customer.prototype.type = "NORMAL";
Customer.prototype.report = function(){
  console.log(this.name);
  console.log(this.orderAmount);
  console.log(this.type);
  console.log(this.country);
};

Customer.prototype.promoteType = function(){
  this.type = "SILVER";
};

var customer1 = new Customer("Customer 1",10);
// public field
customer1.country = "A Country";
customer1.report(); // Customer 1
                     // 10
                     // NORMAL
                     // A Country

var customer2 = new Customer("Customer 2",20);
customer2.promoteType();
console.log(customer2.type); // SILVER
console.log(customer1.type); // NORMAL

公共方法 - Public Methods

原型方法是公共的,所有与之关联的对象或方法也都是公共的。

function Customer(){
  // public method
  this.shipOrder = function(shipAmount){
     return shipAmount;
  };
}

// public method
Customer.prototype.addOrder = function (orderAmount) {
    var totalOrder = 0;
    for(var i = 0; i < arguments.length; i++) {
      totalOrder += arguments[i];
    }
    return totalOrder;
  };

var customer = new Customer();
// public method
customer.findType = function(){
   return "NORMAL";
};

console.log(customer.addOrder(25,75)); // 100
console.log(customer.shipOrder(50)); // 50
console.log(customer.findType()); // NORMAL

继承 - Inheritance

有几种方法可以在JavaScript中实现继承。其中"原型继承"——使用原型机制实现继承的方法,是最常用的。如下面示例:

function Parent(){
  var parentPrivate = "parent private data";
  var that = this;
  
  this.parentMethodForPrivate = function(){
     return parentPrivate;
  };

  console.log("parent");
}

Parent.prototype = {
  parentData: "parent data",
  parentMethod: function(arg){
    return "parent method";
  },
  overrideMethod: function(arg){
    return arg + " overriden parent method";
  }
}

function Child(){
  // super constructor is not called, we have to invoke it
  Parent.call(this);
  console.log(this.parentData);
  var that = this;
  this.parentPrivate = function(){
     return that.parentMethodForPrivate();
  };
  console.log("child");
}

//inheritance
Child.prototype = new Parent();// parent
Child.prototype.constructor = Child;

//lets add extented functions
Child.prototype.extensionMethod = function(){
  return "child’s " + this.parentData;
};

//override inherited functions
Child.prototype.overrideMethod = function(){
  //parent’s method is called
  return "Invoking from child" + Parent.prototype.
  overrideMethod.call(this, " test");
};

var child = new Child();// parent
// parent data
 // child
console.log(child.extensionMethod()); //child’s parent data
console.log(child.parentData); //parent data
console.log(child.parentMethod()); //parent method
console.log(child.overrideMethod()); //Invoking from child test
overriden parent method
console.log(child.parentPrivate()); // parent private data
console.log(child instanceof Parent); //true
console.log(child instanceof Child); //true

当一个成员字段或函数被访问时,会首先搜索这个对象自身的成员。如果没有找到,那么会搜索这个对象对应的原型对象。如果在原型对象中仍然没有找到,那么会在它的父对象中查找成员和原型。这个继承关系也被成为 "原型链"。下面这张图就反映了原型链的继承关系。

模块化 - Modularization

当我们的项目中,自定义的对象类型越来越多时,我们需要更有效地组织和管理这些类定义,并控制他们的可见性,相互依赖关系以及加载顺序。"命名空间"和"模块"能够帮助我们很好地解决这个问题。(EcmaScript 6已经实现了模块系统,但因它还没有被所有浏览器实现,此处我们仍以ES5为例来进行说明)

命名空间 - Namespaces

JavaScript中并没有命名空间的概念。我们需要通过对象来创建命名空间,并将我们定义的对象类型放入其中。

//create namespace
var myLib = {};
myLib.myPackage = {};
//Register types to namespace
myLib.myPackage.MyType1 = MyType1;
myLib.myPackage.MyType2 = MyType2;

模块 - Modules

模块被用来将我们的JavaScript代码分解到包中。模块可以引用其他模块或将自己定义的对象类型对外暴露,以供其他模块使用。同时它能够用来管理模块间的依赖关系,并按照我们指定的顺序进行加载。目前有一些第三方库可以用来实现模块的管理。

下面的例子中,我们在模块里定义新的类型,并且引用其他模块并将自身的公共类型对外暴露。

Module.define("module1.js",
               ["dependent_module1.js","dependent_module2.js",...],
               function(dependentMod1, dependentMod2) {//IMPORTS

  //TYPE DEFINITIONS
  function ExportedType1(){
    // use of dependent module’s types
    var dependentType = new dependentMod1.DependentType1();
    ...
  }

  function ExportedType2(){
  }

  ...
  // EXPORTS
  return { ExportedType1: ExportedType1, ExportedType2:ExportedType2,...};
});

//To use a module (can work asynchronously or synchronously):
Module.use(["module1.js"], function(aModule){
  console.log("Loaded aModule!");
  var AType = aModule.AnExportedType;
  var atype1Instance = new AType();
});

自定义异常 - Custom Exceptions

JavaScript中有一些内部定义的异常,如Error、TypeError和SyntaxError。它们会在运行时被创建和抛出。所有的异常都是"unchecked"。一个普通的对象也可以被用作一个异常,并在throw语句中抛出。因此,我们可以创建自己定义的异常对象,并且在程序中捕获它们进行处理。一个异常处理的最佳实践是,扩展JavaScript中标准的Error对象。

function BaseException() {}
BaseException.prototype = new Error();
BaseException.prototype.constructor = BaseException;
BaseException.prototype.toString = function() {
  // note that name and message are properties of Error
  return this.name + ":"+this.message;
};

function NegativeNumberException(value) {
  this.name = "NegativeNumberException";
  this.message = "Negative number!Value: "+value;
}
NegativeNumberException.prototype = new BaseException();
NegativeNumberException.prototype.constructor = NegativeNumberException;

function EmptyInputException() {
  this.name = "EmptyInputException";
  this.message = "Empty input!";
}
EmptyInputException.prototype = new BaseException();
EmptyInputException.prototype.constructor = EmptyInputException;


var InputValidator = (function() {
  var InputValidator = {};
  InputValidator.validate = function(data) {
    var validations = [validateNotNegative, validateNotEmpty];
    for (var i = 0; i < validations.length; i++) {
      try {
        validations[i](data);
      } catch (e) {
        if (e instanceof NegativeNumberException) {
          //re-throw
          throw e;
        } else if (e instanceof EmptyInputException) {
          // tolerate it
          data = "0";
        }
      }
    }
  };
  return InputValidator;

  function validateNotNegative(data) {
    if (data < 0)
      throw new NegativeNumberException(data)
  }

  function validateNotEmpty(data) {
    if (data == "" || data.trim() == "")
      throw new EmptyInputException();
  }
})();

try {
  InputValidator.validate("-1");
} catch (e) {
  console.log(e.toString()); // NegativeNumberException:Negative number!Value: -1
  console.log("Validation is done."); // Validation is done.
}

自定义事件 - Custom Events

自定义事件能够帮助我们减小代码的复杂度,并且有效地进行对象之间的解耦。下面是一个典型的自定义事件应用模式:

function EventManager() {}
var listeners = {};

EventManager.fireEvent = function(eventName, eventProperties) {
  if (!listeners[eventName])
    return;
  for (var i = 0; i < listeners[eventName].length; i++) {
    listeners[eventName][i](eventProperties);
  }
};

EventManager.addListener = function(eventName, callback) {
  if (!listeners[eventName])
    listeners[eventName] = [];
  listeners[eventName].push(callback);
};

EventManager.removeListener = function(eventName, callback) {
  if (!listeners[eventName])
    return;
  for (var i = 0; i < listeners[eventName].length; i++) {
    if (listeners[eventName][i] == callback) {
      delete listeners[eventName][i];
      return;
    }
  }
};

EventManager.addListener("popupSelected", function(props) {
  console.log("Invoked popupSelected event: "+props.itemID);
});
EventManager.fireEvent("popupSelected", {
  itemID: "100"
}); //Invoked popupSelected event: 100

编写组件 - Components

JavaScriipt允许开发人员通过编写组件来向HTML元素添加行为。下面是一个典型的应用场景,将JavaScript对象绑定到了DOM元素上。

1-定义JavaScript组件

function InputTextNumberComponent(domElement) {
  this.initialize(domElement);
}

InputTextNumberComponent.prototype.initialize =
  function(domElement) {
    domElement.onchange = function() {
      //just a format
      domElement.value = "-" +domElement.value + " -";
    };
    domElement.jsComponent = this; //Expando property
    this.domElement = domElement;
  };
InputTextNumberComponent.prototype.resetValue = function() {
  this.domElement.value = "";
};

2-定义一个CSS样式用于与JavaScript组件写作控制HTML元素

<style type="text/css">
  .inputTextNumber { text-align:right; }
</style>

HTML元素的定义如下

<input type="text" class="inputTextNumber" name="NumberField"
size="10" value="Click me!" onClick="this.jsComponent.
resetValue()">

2-当页面加载时(或DOM元素都已准备就绪),检测HTML元素并创建JavaScript组件

window.onload = function() {
  var inputTextNumbers = document.getElementsByClassName("inputTextNumber");
  for (var i = 0; i < inputTextNumbers.length; i++) {
    var myComp = new InputTextNumberComponent(
      inputTextNumbers.item(i));
  }
};

简书签约作者:技匠,以上内容欢迎大家分享到朋友圈/微博等。如需转载,请通过简信联系授权。谢谢大家!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,661评论 0 15
  • 万里云动风墨色 千里冰封万里 各自无言以对 沉默的螺旋 迎风而止 封冰云中歌 无话坚冰 黎明踏浪...
    my9311阅读 271评论 2 5
  • 在开发中我们经常需要在控制台中打印出一些数据,以验证我们代码的正确性。一般我们的需求都是会打印出网络请求的返回结果...
    天天想念阅读 3,134评论 0 5
  • 从广义上说,教育应该教什么?用三个关键词概括就是:认识、思考、改变。 教育在认识层面的意义,是我们最为熟知的意义,...
    蜡笔小新有个妹妹阅读 526评论 0 1